# rat-stack 🐀 _An Effect stack so pure (aspirational) Kit Langton will blush._ [![CI](https://github.com/joelhooks/rat-stack/actions/workflows/ci.yml/badge.svg)](https://github.com/joelhooks/rat-stack/actions/workflows/ci.yml) Build an Effect app and its cloud as one typed program. Joel's goal is the best Effect + Alchemy application we can build; `VISION.md` explains why. `AGENTS.md` sets exact pins, checks, and hooks. The public tree lets you steal ideas. It carries no product support promise. The **pnpm + Turborepo workspace** includes a working Effect v4 CLI, tests, formatting, and type-aware linting. Vendored source mirrors cover Effect, effect-solutions, XState, and Alchemy. Define a **Contract** with Effect input, output, and failure schemas plus annotations. Bind a server-side handler, then project the capability onto every agent surface. The same `inspectFile` capability is the `stats` command, `POST /inspectFile` with an OpenAPI document, and an MCP tool over stdio. ## Create a repository ```sh gh repo create --template joelhooks/rat-stack ``` Clone the new repository, then install and verify it: ```sh nvm use pnpm install pnpm vendor:agent-sources pnpm turbo run check test build ``` Node `24.18.0` and pnpm `11.3.0` are required. The requirement is declared in `.nvmrc`, `engines`, `devEngines`, and `packageManager`. ## Workspace layout - **`apps/cli`**: - Package: `@rat-stack/cli` - Role: Composition root: `stats`, `openapi`, `serve`, `mcp` - **`packages/capability`**: - Package: `@rat-stack/capability` - Role: `defineContract`, `implement`, and the `toCommand`, `toHttpApi`, `toToolkit`, `toRpc`, and `toCodeMode` projections - **`packages/core`**: - Package: `@rat-stack/core` - Role: Domain example: the `inspectFile` contract, handler, and lifecycle machine - **`packages/flags`**: - Package: `@rat-stack/flags` - Role: Typed flags, Config, memory and KV adapters, stable rollouts, audited writes, and removal dates - **`packages/events`**: - Package: `@rat-stack/events` - Role: Request analytics: capture, visitor privacy rules, and Basin resources behind core's `EventSink` port - **`packages/crash`**: - Package: `@rat-stack/crash` - Role: Private crash-trace archive for the Mischief Worker - **`packages/lore`**: - Package: `@rat-stack/lore` - Role: Public content graph and traversal service - **`packages/subscriber-delivery`**: - Package: `@rat-stack/subscriber-delivery` - Role: External subscriber intake and confirmation adapter behind core's job-shaped ports - **`apps/infra`**: - Package: `@rat-stack/infra` - Role: Alchemy Stack (Cloudflare by default) - **`apps/site`**: - Package: `@rat-stack/site` - Role: Cloudflare Worker for the public site, agent discovery, and sandboxed execute - **`.agent_sources/`**: - Package: — - Role: Shallow upstream mirrors (gitignored clones; see README there) ## Try the example CLI The included `stats` command counts bytes, Unicode characters, words, and lines in a file: ```sh pnpm cli stats README.md pnpm cli stats README.md --json ``` After `pnpm build`, you can run the built entrypoint directly: ```sh node apps/cli/dist/cli.js stats README.md ``` The same capability on the other surfaces: ```sh pnpm cli openapi # OpenAPI 3.1 document for the REST projection pnpm cli serve --port 3000 # POST /inspectFile, GET /openapi.json, GET /docs pnpm cli mcp # MCP server over stdio, one tool per capability pnpm cli mcp --code-mode # MCP server with two tools: search and execute pnpm cli catalog --types # the `tools` declarations a code-mode program sees ``` Code mode is the fourth projection. The model gets `search` for ranked matches with TypeScript signatures and `execute` for JavaScript with `tools` in scope. The program runs in a fresh Node subprocess under `--permission`. It cannot touch the file system or spawn processes. To call the host, it uses `tools.(input)`. The host validates the input schema and runs the same handler used by every other surface. Node's permission model allows network egress. Put a Worker or Deno runtime behind the same `Sandbox` service to isolate the network too. `packages/capability/src` is where a capability becomes a `Command`, an `HttpApiEndpoint`, a `Tool`, and a catalog entry. `packages/core/src/inspect-file.ts` supplies the CLI's example capability. Hosted content has its own registry in `apps/site/src/capabilities/index.ts`. Provider adapters stay outside core (`rat-stack-boundaries/no-core-adapters`); apps provide them to core's job-shaped ports at composition. Add another to `capabilities`, then verify every projection you keep; CLI command registration lives in `apps/cli/src/command.ts`. ## What is in the stack? - **pnpm workspaces + Turborepo `2.11.9`**: cached `typecheck` / `test` / `build` across packages from day one. - **Effect `4.0.7`**: typed runtime, errors, filesystem service, and the three surfaces the capabilities project onto: `effect/cli`, `effect/http-api` (with `OpenApi.fromApi`), and `effect/ai` (`Toolkit` + `McpServer`). - **`@effect/platform-node` `4.0.7`**: Node-backed services; keep adapter and core pins matched. - **TypeScript `7.0.2`**: strict module and index-access checks. - **`@yielded/effect-tsrs` `0.2.3`**: runs TypeScript and Effect diagnostics in `typecheck` and `check`. `--type-check` includes ordinary TypeScript diagnostics; `--strict` makes Effect warnings fail. It bundles tsrs `8f135f106a63`, contained in plain tsrs `0.20.0`. - **tsrs `0.20.1-ts7.1.0-beta`**: experimental compiler targeting TypeScript `7.1.0-beta` for `typecheck:fast`. It still has no Effect diagnostics; use `typecheck` or `check` for those. - **XState `6.0.0-alpha.71`**: real lifecycle states; do not replace those with boolean soup. - **`@xstate/effect` `1.0.0-alpha.10`**: the official XState v6 to Effect 4 bridge: `createEffectActor` runs a machine as a scoped Effect, `createEffectLogic` makes Effects into actors with typed failures and requirements. `packages/core/src/inspect-machine.ts` is the example. - **Alchemy `2.0.0-beta.84`**: [Infrastructure as Effects](https://alchemy.run): `apps/infra/alchemy.run.ts` is the Stack; cloud providers come from `@alchemy.run/cloudflare`, `@alchemy.run/drizzle`, and `@alchemy.run/sql`; `pnpm infra:plan` / `infra:deploy` / `infra:destroy`; auth via `pnpm alchemy profile edit`. - **Foldkit `0.169.0` + `@foldkit/vite-plugin` `0.28.0`**: the web UI. Server rendering and prerendering are on by default. One `vite build` writes the browser bundle, a server `fetch` function, and an HTML file for every public page. `Cloudflare.Website.Foldkit` serves the files and sends every other request to that function. The browser hydrates the same view. `.brain/areas/web.svx` is the system page. - **Oxlint + Ultracite + Oxfmt**: native lint and format. - **Vitest `5.0.5` + `@effect/vitest` `4.0.7`**: `it.effect` and `it.layer` for every Effect test; running Effects by hand in a test file is a lint error. - **`@effect/tsgo` `0.55.0`**: patches TypeScript 7 in `prepare` for `typecheck:tsc`. Effect warnings fail because `ignoreEffectWarningsInTscExitCode` stays false. Patched `tsc` runs at the switch and on checker bumps through parity, not on every CI run. Report any checker disagreement. - **varlock `1.23.0`**: `.env.schema` declares every variable with `@env-spec` decorators; `pnpm check` runs `varlock load`, secrets stay in gitignored `.env.local`. - **Vendored agent sources**: Effect, [effect-solutions](https://github.com/kitlangton/effect-solutions), XState, [Alchemy](https://github.com/alchemy-run/alchemy), [Better Auth](https://github.com/better-auth/better-auth), and [Foldkit](https://github.com/foldkit/foldkit) via `./scripts/vendor-agent-sources.sh`, each at the ref its workspace pin names (not x-algorithm; that stays app-specific). - **Agent fence**: lefthook pre-commit + Pi/Cursor/Claude hooks that block `git … --no-verify`. Cheating should be uncomfortable and obvious. Every dependency is pinned exactly. Review pin upgrades as stack changes. ## Commands - **`pnpm typecheck`**: Check TypeScript and Effect diagnostics with strict effect-tsrs - **`pnpm typecheck:tsc`**: Run patched TypeScript `7.0.2` / `@effect/tsgo` `0.55.0`; runs at checker bumps, not in CI - **`pnpm typecheck:fast`**: Fast feedback with tsrs; no Effect diagnostics; `pnpm check` stays the gate - **`pnpm typecheck:parity`**: Compare effect-tsrs and patched `tsc` errors and exit verdicts, including `TS377xxx`; also compare plain tsrs with Effect-only codes excluded - **`pnpm bump:checkers`**: Bump both exact checker pins to npm latest, report bundled revision alignment, and run both parity pairs - **`pnpm check`**: Typecheck, check formatting, and run type-aware linting - **`pnpm fix`**: Apply Oxfmt and safe Oxlint fixes - **`pnpm test`**: Run the Vitest suite once - **`pnpm build`**: Compile package outputs - **`pnpm cli -- ...`**: Build and run the CLI - **`pnpm vendor:agent-sources`**: Clone core lib mirrors - **`pnpm turbo run check test build`**: Cached verification pipeline ## Agentic surface Every clone includes `AGENTS.md` for repo law and commands, `CLAUDE.md` as Claude Code's pointer to that law, and `VISION.md` for project intent. Source mirrors live under `.agent_sources/` after you run the vendor script. Repo-local Pi extensions belong in `.pi/extensions/`. ## Make it yours Start with the template: ```sh gh repo create --template joelhooks/rat-stack ``` Clone the new repository, then install it: ```sh pnpm install pnpm sources:fetch ``` `pnpm sources:fetch` imports missing pinned source commits from the configured repository remotes once, including in template children. Generation reads only local Git objects and never fetches. Re-run the command when adding a new pin that is not present locally. Rename the workspace package names before you build on the example. The current `name` fields are: - `package.json`: `rat-stack` - `apps/cli/package.json`: `@rat-stack/cli` - `apps/infra/package.json`: `@rat-stack/infra` - `apps/site/package.json`: `@rat-stack/site` - `packages/capability/package.json`: `@rat-stack/capability` - `packages/core/package.json`: `@rat-stack/core` Rename the `bin` entry in `apps/cli/package.json`, the root command in `apps/cli/src/command.ts`, and the version in `apps/cli/src/version.ts`. Then change every matching import. The current workspace imports are `@rat-stack/capability`, `@rat-stack/cli`, `@rat-stack/core`, `@rat-stack/infra`, and `@rat-stack/site`. Find them with: ```sh rg -n '@rat-stack/' --glob '*.json' --glob '*.ts' -l ``` A new scope changes line lengths, so run the formatter once after the rename: ```sh pnpm fix ``` Replace `inspectFile` in `packages/core` with one useful capability. Add it to `capabilities`, then verify each surface you keep. Rewrite the Project law, Architecture, and Boundaries sections of `AGENTS.md` and the top of `.pi/APPEND_SYSTEM.md`; they describe rat-stack until you do. Keep expected failures typed and map them to deliberate exit codes. `apps/site` is the public site and Worker. `apps/infra` is its Alchemy Stack. Delete both if you do not want a public site. If you keep them, change the Cloudflare Zone name and the DNS names and targets derived from it in `apps/infra/alchemy.run.ts`. Change the Worker domain and redirects in `apps/site/src/worker.ts`. Choose an Alchemy stage for each plan or deploy. The stage is a command-line choice, for example `--stage `. Configure credentials with: ```sh pnpm alchemy profile edit --add Cloudflare ``` Run the acceptance gate before the first push: ```sh pnpm turbo run check test build ``` ## What it promises and how to check - One capability reaches every kept surface. Proof: `packages/capability/test/to-command.test.ts`, `packages/capability/test/to-http-api.test.ts`, `packages/capability/test/to-toolkit.test.ts`, `packages/capability/test/to-code-mode.test.ts`, and `apps/cli/test/cli.e2e.test.ts`. - Schemas are the contract. Proof: `packages/capability/test/catalog.test.ts`, `packages/capability/test/to-http-api.test.ts`, and `packages/capability/test/to-toolkit.test.ts` check derived JSON Schema and typed failures. - A fresh sandbox isolates model code. The Node sandbox has a 10-second wall-clock limit and a 1-second limit per synchronous evaluation. Both run inside the child. Scope close and interruption send SIGTERM, then SIGKILL after 250 milliseconds. Stdin EOF, a lost parent, or a failed protocol write ends the child. Child-process imports remain blocked. Proof: `packages/capability/test/sandbox.test.ts` checks isolation and VM budgets; `sandbox-lifetime.test.ts` and `sandbox-orphan.test.ts` check deadlines, parent death, and process-group cleanup. - Rate limits fail closed. Proof: `apps/site/test/worker.test.ts` checks 429 responses before another execute worker starts and when the global limit denies. - Markdown is the default machine representation. Proof: `apps/site/test/worker.test.ts` checks Markdown without `Accept: text/html` and HTML only when requested. ## Keep or cut The template is a working project. Delete what you will not use on day one; the fence will identify dependent code that must go too. After each cut, trim `packages/capability/src/index.ts` and run `pnpm install`, `pnpm fix`, then `pnpm turbo run check test build`. Update the package list in `AGENTS.md`. - **Only the CLI**: - Keep: `packages/capability/src/contract.ts`, `packages/capability/src/implement.ts`, `packages/capability/src/to-command.ts`, `call-watch.ts`, `actor-watch.ts`, every dependency imported by retained code, and their tests; all of `packages/core`; the `toCommand` composition in `apps/cli/src/surfaces.ts` - Delete: unused projections and their matching tests after tracing the retained imports; unused compositions in `apps/cli/src/surfaces.ts`; the `catalog`, `openapi`, `serve`, and `mcp` commands in `apps/cli/src/command.ts`; `apps/cli/test/serve.test.ts` and the MCP and catalog cases in `apps/cli/test/cli.e2e.test.ts` - **No code mode**: - Delete: `packages/capability/src/catalog.ts`, `packages/capability/src/code-mode.ts`, `packages/capability/src/sandbox-error.ts`, `packages/capability/src/sandbox-service.ts`, `packages/capability/src/sandbox-subprocess.ts`, and `packages/capability/src/to-code-mode.ts`; `packages/capability/test/catalog.test.ts`, `packages/capability/test/sandbox.test.ts`, `packages/capability/test/sandbox-lifetime.test.ts`, `packages/capability/test/sandbox-orphan.test.ts`, `packages/capability/scripts/sandbox-lifetime-proof.mjs`, and `packages/capability/test/to-code-mode.test.ts`; the `./sandbox` and `./code-mode` exports in `packages/capability/package.json`; `codeMode` and `mcpServer.codeMode` in `apps/cli/src/surfaces.ts`; the `catalog` command and the `--code-mode` flag in `apps/cli/src/command.ts`; the code-mode and catalog cases in `apps/cli/test/cli.e2e.test.ts` - **No HTTP**: - Delete: `packages/capability/src/to-http-api.ts`, `packages/capability/src/http-api.ts`, and their tests; the `./http-api` export in `packages/capability/package.json`; `http`, `routes`, and `webServer` in `apps/cli/src/surfaces.ts`; the `openapi` and `serve` commands; `apps/cli/test/serve.test.ts`; the OpenAPI case in `apps/cli/test/cli.e2e.test.ts` - **No MCP**: - Delete: `packages/capability/src/to-toolkit.ts`, `packages/capability/src/toolkit.ts`, their test, and `packages/capability/test/mcp-harness.ts`; the `./toolkit` export in `packages/capability/package.json`; `tools` and `mcpServer` in `apps/cli/src/surfaces.ts`; the `mcp` command; the MCP cases in `apps/cli/test/cli.e2e.test.ts`. Code mode imports from `packages/capability/src/to-toolkit.ts`, so cutting MCP cuts code mode too - **Default flags only**: - Keep: Core's `Flags` port and declarations - Delete: Replace `apps/site/src/flags.ts` with core imports and `export const flagsLayer = Flags.defaults(eventFlags); export const liveFlagsLayer = flagsLayer;`; remove `packages/flags` and its dependency in `apps/site/package.json`. Readers use defaults; the CLI writer fails with a named read-only error - **No analytics**: - Delete: `packages/events`; the `@rat-stack/events` line in `apps/site/package.json` - Change: the one line in `apps/site/src/events.ts` becomes `export { events, memoryEvents } from "@rat-stack/core/events-off";` - Then: `pnpm install`, `pnpm git-snapshot`, and the gate - Optional: `EVENTS_ENABLED`, `EVENTS_SINK_TOKEN`, and `EVENTS_IDENTITY_MODE` in `apps/site/.env.schema` and `apps/site/src/config-fingerprint.ts`, and the events flags in `packages/core/src/flags.ts`. The Worker ignores them once capture is off. Details: `packages/events/README.md` - **No XState (only after removing interest and every other lifecycle except file inspection)**: - Keep: Keep XState and its bridge while any retained lifecycle imports them - Delete: `packages/core/src/inspect-machine.ts` and its test (call `FileInspector.inspect` directly from `packages/core/src/inspect-file.ts`); `xstate` and `@xstate/effect` in `packages/core/package.json` and their `minimumReleaseAgeExclude` entries in `pnpm-workspace.yaml`; `scripts/oxlint-plugin-xstate-effect.ts` and its entry in `oxlint.config.ts` `defineContract`, `implement`, and `toCommand` are the minimum that keep `stats` working. The contract module has no dependency on the other projections. ## License MIT