# ratstack.sh

> Effect-native TypeScript capabilities with CLI, HTTP, MCP, and sandboxed code-mode projections.

## Entry points

- [Repository map](https://ratstack.sh/): concise human and agent overview
- [Full corpus](https://ratstack.sh/llms-full.txt): all public law and skill documents in one response
- [OpenAPI](https://ratstack.sh/openapi.json): generated HTTP capability contract
- [MCP](https://ratstack.sh/mcp): stateless MCP 2026-07-28 endpoint

## Law

- [Repository law](/AGENTS.md) — Commands, architecture constraints, boundaries, and contribution rules.
- [Product vision](/VISION.md) — Why rat-stack exists and what a successful clone should preserve.
- [Rat-stack README](/README.md) — The stack, shipped example, surfaces, and first-run instructions.
- [Vendoring policy](/vendor/README.md) — Rules for pinned unpublished packages and eventual removal.
- [Workspace pins](/pins.md) — Exact dependency values declared by every workspace package.
- [Effect 4 reference projects](/resources/effect-4-reference-projects.svx) — Source-grounded reference implementations for this pinned Effect release.
- [Schema projections and code mode](/resources/schema-projections-and-code-mode.svx) — The architectural reasoning behind capabilities and their projections.

## Skills

- [add-a-capability](/skills/add-a-capability) — Add one schema-typed behavior to rat-stack and project it through CLI, HTTP, MCP, and code mode without duplicating handlers.
- [add-a-lifecycle-machine](/skills/add-a-lifecycle-machine) — Add an Effect-backed XState lifecycle for finite states, retries, cancellation, or resumable work in rat-stack.
- [keep-or-cut](/skills/keep-or-cut) — Remove unused rat-stack surfaces from a new clone while preserving the smallest working capability-to-CLI scaffold and its fence.
- [learn-rat-stack](/skills/learn-rat-stack) — Understand rat-stack before changing a clone, choosing a surface, adding behavior, or trimming the template.


---

# /AGENTS.md

Source: AGENTS.md
SHA-256: 51b8ce07dff75cbc4e5b8d567178f69ab8601cb20b0c082caea5e6a09bc44fe0

# Agent instructions

This file is the repo law for agents and contributors. Keep commands, validation rules, architecture constraints, and project-specific stop rules here. Read `VISION.md` for product intent before planning substantial work. `VISION.md` is not permission to bypass this file. Pi sessions also load `.pi/APPEND_SYSTEM.md` (project context) and the live repo-local extension `.pi/extensions/project.ts`.

## Stack contract

The pinned stack is declared in workspace `package.json` files and summarized in [README.md](./README.md#what-is-in-the-stack). Keep dependencies exact. Repo-local config wins; note drift instead of silently migrating the project.

- pnpm workspaces + Turborepo (`apps/*`, `packages/*`)
- Node `>=24.18.0` and pnpm `11.3.0`; do not replace pnpm with Bun or npm for installs
- Effect `4.0.0-rc.116` and `@effect/platform-node` `4.0.0-rc.116`
- XState `6.0.0-alpha.58` for finite lifecycles, retries, cancellation, and resumability
- `@xstate/effect` `0.1.0-alpha.2` bridges the two: machines run as scoped Effects via `createEffectActor`, side effects are declared `fromEffect` actors. Published to npm 2026-09-19; `vendor/README.md` keeps the rules for the next unpublished pin
- Alchemy `2.0.0-beta.79` (Infrastructure as Effects) for every cloud resource; declared in `apps/infra/alchemy.run.ts`, authenticated through Alchemy profiles, never through env vars in this repo
- TypeScript `7.0.2` in strict mode, patched by `@effect/tsgo` `0.45.0` in `prepare` so the Effect language service diagnostics in `tsconfig.base.json` fail `tsc`, not just the editor. Escape hatch for a real boundary: `// @effect-diagnostics-next-line <rule>:off` with a reason
- `@effect/vitest` `4.0.0-rc.116` for every Effect test: `it.effect` and `it.layer(layer)`; `Effect.run*` and `ManagedRuntime.make` in test files are a lint error
- `@oxlint/plugins` `1.83.0` for the two typed lint rules in `scripts/oxlint-plugin-*.ts`
- Oxlint `1.83.0` with Ultracite `7.12.0`, Oxfmt `0.68.0`, and Turborepo `2.11.2`
- varlock `1.20.0`: declare every env var in `.env.schema`, never read `.env.local` directly, run `pnpm env:check` after schema edits

## Packages

| Package | Path | Role |
| --- | --- | --- |
| `@rat-stack/capability` | `packages/capability` | `defineCapability` and the projections `toCommand`, `toHttpApi`, `toToolkit`, `toCodeMode` (catalog, `search`/`execute`, subprocess `Sandbox`) |
| `@rat-stack/core` | `packages/core` | Domain logic: the `inspectFile` capability, its lifecycle machine, `FileInspector` |
| `@rat-stack/cli` | `apps/cli` | Composition root: `stats`, `catalog`, `openapi`, `serve`, `mcp [--code-mode]` commands |
| `@rat-stack/infra` | `apps/infra` | Alchemy Stack: the project's cloud footprint as one Effect program |

## Commands

| Command | Purpose |
| --- | --- |
| `pnpm install` | Install workspace dependencies |
| `pnpm check` | Typecheck, verify formatting, and run type-aware linting |
| `pnpm lint` | Type-aware lint plus format check only; `turbo run check` runs this once at the root as `//#lint` |
| `pnpm fix` | Apply Oxfmt and safe Oxlint fixes |
| `pnpm test` | Build and run the Vitest suite once |
| `pnpm build` | Compile packages into `dist/` |
| `pnpm typecheck` | `turbo run typecheck` |
| `pnpm vendor:agent-sources` | Shallow-clone Effect, effect-solutions, xstate, and alchemy mirrors |
| `pnpm infra:plan` | Preview the Alchemy Stack diff without applying |
| `pnpm infra:deploy` / `pnpm infra:destroy` | Apply or tear down the Stack (asks for approval) |
| `pnpm exec lefthook install` | Install git hooks (also via `prepare`) |
| `pnpm turbo run check test build` | Required validation before claiming a change is ready |

Run `pnpm fix` only when you intend to rewrite files. Finish with `pnpm turbo run check test build`.

## Fence (cheating is uncomfortable)

Why lives in `AGENTS.md` / `VISION.md`. The stack and hooks are the enforceable fence. Fence wins over prose.

- Lefthook pre-commit runs `pnpm check` and `pnpm test`
- Agents must not use `git … --no-verify` (or equivalent hook bypass)
- Blocked by: Pi `.pi/extensions/git-interceptor`, Cursor `.cursor/hooks.json`, Claude Code `.claude/settings.json`
- Policy source: `scripts/vcs-command-policy.js`

If a hook fails, fix the failure. Do not disable the fence.

## Source-first Effect / XState work

Before writing, reviewing, or refactoring Effect or XState code, read `node_modules/effect/AGENTS.md` first: it ships with the installed version, so it is never stale. Then inspect vendored source for the pinned versions. Populate mirrors:

```sh
pnpm vendor:agent-sources
# or
./scripts/vendor-agent-sources.sh --refresh
```

Inventory: [`.agent_sources/README.md`](./.agent_sources/README.md).

| Need | Path |
| --- | --- |
| Effect Schema, Context.Service, CLI | `.agent_sources/github.com/Effect-TS/effect/` |
| Idiomatic Effect | `.agent_sources/github.com/kitlangton/effect-solutions/` |
| XState | `.agent_sources/github.com/statelyai/xstate/` |
| `@xstate/effect` (v6 Effect bridge) | `.agent_sources/github.com/statelyai/xstate/packages/xstate-effect/` (`README.md`, `docs/`, `src/*.test.ts`) |
| Alchemy resources, Cloudflare, AWS | `.agent_sources/github.com/alchemy-run/alchemy/` and https://alchemy.run/llms.txt |

Refs are derived from the workspace pins, so bumping a package and re-running the script keeps them matched. The script also links `.agent-sources/effect` (what the `pi-effect` tool reads) to the pinned Effect mirror.

Mirrors are reference material, not runtime dependencies. Exclude them from typecheck, test, lint, and format. Do not vendor product-specific corpora in this template.

## Source control

Preserve existing work. Inspect status before editing, stage only files changed for the current task, and do not commit generated output, secrets, or populated `.agent_sources/github.com/` trees. Use the repository's existing source-control tool; do not initialize or migrate one without approval. Never pass `--no-verify` to git.

## Project law

<!-- A child project replaces this section on day one with its own product rules. These are rat-stack's rules; they describe the template, not your product. Keep durable product intent in VISION.md, not here. -->

- One capability, every surface. Behavior enters through `defineCapability` in `packages/core` and is added to `capabilities`; CLI, HTTP, MCP, and code mode are projections in `packages/capability`. Do not add a command, route, or tool handler that bypasses a capability.
- Schemas are the contract. Input, output, and failure are Effect `Schema`; JSON Schema, OpenAPI, and the code-mode declarations are derived from them, never hand-written.
- Lifecycles are machines. Finite modes, retries, and cancellation live in XState machines started with `createEffectActor`; side effects live in declared `fromEffect` actors, never inline.
- The sandbox is a surface, not a bypass. Anything reachable from a code-mode program must be a capability and goes through that capability's schemas and handler.
- Diagnostic overrides are targeted and explained: `// @effect-diagnostics-next-line <rule>:off` with a reason. File-level overrides exist only at projection boundaries (`to-toolkit.ts`, `to-code-mode.ts`) and in process-spawning test files.
- Pins stay exact. A vendored dependency carries a matching `minimumReleaseAgeExclude` entry and a removal rule in `vendor/README.md`.

## Architecture

<!-- A child project replaces this section on day one. Record module boundaries, dependency direction, data ownership, and state-machine seams; link deeper docs instead of duplicating them. -->

- Dependency direction: `apps/cli` → `packages/core` → `packages/capability` → Effect/XState. Packages do not import apps. `core` owns domain behavior and depends on `capability` only for `defineCapability`; `capability` knows nothing about the domain.
- `packages/core/src/file-inspector.ts` is the reference service shape: a `Context.Service` class whose `make` captures its dependencies so its methods carry no requirements, with `static layer` beside it. `packages/core/src/config-service.ts` derives a service from Effect `Config` (production `layer` reads the ConfigProvider, `configLayer` takes parsed values for tests); `AppConfig` is the instance and mirrors `.env.schema`.
- `packages/core/src/inspect-machine.ts` is the reference shape for a lifecycle: the machine owns states, declared `fromEffect` actors own side effects and typed failures, `join` plus `Effect.orDie` hands the outcome back to Effect. `packages/core/src/inspect-file.ts` wraps it as the one shipped capability.
- Effect-backed machines start only under `createEffectActor`, never `createActor`. Only actions and actors declared in `setupEffect` contribute to the actor's requirements; the `xstate-effect/no-inline-effect` lint rule enforces the inline cases.
- `packages/capability/src`: `capability.ts` (the domain object), `to-command.ts`, `to-http-api.ts`, `to-toolkit.ts` (pure projections onto `effect/unstable/{cli,httpapi,ai}`), `catalog.ts` + `sandbox.ts` + `to-code-mode.ts` (the fourth projection; `Sandbox` is a `Context.Service` with a subprocess implementation). `to-code-mode.ts` imports from `to-toolkit.ts`, so MCP is a prerequisite for code mode.
- `apps/cli/src/surfaces.ts` is the only place projections are instantiated; `command.ts` maps them to subcommands; `cli.ts` is the single composition root that provides `FileInspector` and `NodeServices`.
- The [README's Keep or cut section](./README.md#keep-or-cut) lists what to delete per surface.

## Boundaries and sign-off

<!-- A child project replaces this section on day one. Name changes agents may make directly and changes that need owner approval: security, privacy, deployment, public API, dependency, and destructive-data boundaries. -->

- Safe by default: adding a capability in `packages/core` and wiring it into `capabilities`; new or tightened tests; a targeted diagnostic override with a written reason; README, AGENTS.md, and `.brain/` edits; tightening a lint rule; removing a surface by following Keep or cut.
- Needs owner sign-off: adding or changing a dependency version (pins are exact and CI installs cold); any edit to `oxlint.config.ts`, the diagnostics map in `tsconfig.base.json`, `lefthook.yml`, or `scripts/vcs-command-policy.js` that loosens the fence; anything under `apps/infra` and any `pnpm infra:deploy` or `infra:destroy`; widening sandbox permissions or adding a runtime that reaches the network; exposing `serve` or `mcp` beyond localhost; vendoring a package as a `file:` tarball; deleting `.agent_sources/` or `vendor/`.


---

# /VISION.md

Source: VISION.md
SHA-256: 698385b0369b5352e5b763e7fc7cc5dfb8557cde81f975db38561ba7480fac23

# Vision

This repo is an **agentic scaffold** for a TypeScript Effect app: CLI, XState lifecycles, Alchemy infrastructure, and varlock config. It began life as ts-cli-template. It is Joel's opinionated default for starting new work: why in the law files, an enforceable fence in the stack and hooks, so agents write good TypeScript because the cheap path is the honest path.

Public GitHub is a **steal the ideas** surface, not a product to support. Clones that become a real app should replace this vision with that product's intent. Until then, this thesis is the why.

**Scope:** `joelhooks/rat-stack` — the template repo and the shape a clone inherits on day one.

**Audience:** Joel, agents working in a clone, and anyone reading the public tree for ideas. Not paying users. Not a support queue.

## Who we serve

- Primary: Joel and the agents that start work from this scaffold
- Secondary: readers who steal the pattern (workspaces, source mirrors, fence that makes `--no-verify` fail loud)
- Not for: ghostwriting, growth automation, "thin npm init with extra steps," or a general TypeScript tutorial brand

## Why it exists

Agents will take the shortest path. A skeleton without law and gates produces sludge. A pile of prose without a fence is documentation. This scaffold exists so starting a CLI already includes:

1. Why — `VISION.md` and `AGENTS.md`
2. Fence — pins, `pnpm check`, lefthook, and harness hooks that block hook bypass
3. Context — vendored Effect / effect-solutions / XState sources for source-first edits

The stack is the load-bearing floor. The agent surface is the product.

## Outcomes

- A new CLI clone starts as a pnpm + Turborepo workspace with a real Effect CLI, not a single-folder lie
- Agents hit a loud failure when they cheat (`git … --no-verify`, skipped checks) instead of a silent green
- Why and fence stay split: prose explains; CI, lefthook, and agent hooks enforce
- Good TypeScript is the default path: strict TS, typed Effect errors, XState for real lifecycles
- Product-specific corpora (tokens, x-algorithm, customer data) stay out of the shared template

## Current priorities

1. Keep the scaffold honest: fence wins; cheating is uncomfortable and obvious
2. Keep pins and vendor refs matched to the stack actually used
3. Keep the example CLI small; the scaffold is the product, not `stats`

## Actors

- Beneficiary: Joel starting a CLI (and agents in that clone)
- Builders: anyone editing the template
- External systems: GitHub template clone, CI, Pi / Cursor / Claude hook runtimes
- Not an audience: people expecting support, compatibility, or a framework

## Merge by default

- Tests and checks that encode the existing fence
- Docs that sharpen why vs fence without adding a second product promise
- Small example-CLI fixes that keep the vertical slice working
- Pin bumps that stay exact and reviewed as stack changes

## Needs sign-off

- Weakening lefthook, CI, or `--no-verify` blocks
- Replacing pnpm / turbo / Effect / XState as the default floor
- Adding product-specific vendor corpora to the shared template
- Turning the public repo into a supported starter product

## Will not do for now

- Support SLAs or "works on every agent harness"
- Vendoring x-algorithm or other app-specific source
- Generating publishable copy as part of the template
- Bun or npm as the install story
- A gut-me-on-day-one scaffold with no fence

## Decision boundaries

- Safe by default: changes that keep why in VISION/AGENTS and the fence enforceable
- Needs owner sign-off: new promises, toolchain swaps, or making the fence optional
- Evidence expected: `pnpm check` / `pnpm test` green; hook policy tests that still block `git commit --no-verify`

## Amendment policy

This document changes when the scaffold thesis is wrong, not when a clone's product is different. Agents may propose amendments with receipts. Joel approves. A clone that is now a real product should write its own `VISION.md` instead of stretching this one.


---

# /README.md

Source: README.md
SHA-256: e1c066fd270a3cfa704421ae904574196ed41bdb16a31d00a923904d5078c0d1

<p align="center"><img src="assets/ratstack-logo.svg" alt="Rat Stack: a rat sitting on a three-layer stack" width="420"></p>

# rat-stack 🐀

[![CI](https://github.com/joelhooks/rat-stack/actions/workflows/ci.yml/badge.svg)](https://github.com/joelhooks/rat-stack/actions/workflows/ci.yml)

Joel's **agentic scaffold** for an Effect app (CLI, XState lifecycles, Alchemy infra, varlock config): why in `VISION.md` / `AGENTS.md`, fence in pins + checks + hooks. Public tree is steal-the-ideas, not a supported product. It ships as a **pnpm + Turborepo workspace** with a real Effect v4 CLI, tests, formatting, type-aware linting, and vendored source mirrors for Effect, effect-solutions, XState, and Alchemy.

The shape it teaches: define a **Capability** once (Effect Schema in, out, and failure; an Effect handler; read-only / destructive / approval annotations) and project it 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 <name> --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

| Path | Package | Role |
| --- | --- | --- |
| `apps/cli` | `@rat-stack/cli` | Composition root: `stats`, `openapi`, `serve`, `mcp` |
| `packages/capability` | `@rat-stack/capability` | `defineCapability` plus `toCommand`, `toHttpApi`, `toToolkit` |
| `packages/core` | `@rat-stack/core` | Domain example: the `inspectFile` capability and its lifecycle machine |
| `apps/infra` | `@rat-stack/infra` | Alchemy Stack (Cloudflare by default) |
| `.agent_sources/` | — | 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` (ranked matches with TypeScript signatures) and `execute` (a JavaScript program with `tools` in scope). The program runs in a fresh Node subprocess under `--permission`, so it cannot touch the file system or spawn processes; its only way out is `tools.<name>(input)`, which the host validates against that capability's input schema and runs through the same handler as every other surface. Network egress is not blocked by Node's permission model; put a Worker or Deno runtime behind the same `Sandbox` service for real isolation.

`packages/capability/src` is where a capability becomes a `Command`, an `HttpApiEndpoint`, a `Tool`, and a catalog entry. `packages/core/src/inspect-file.ts` is the one capability shipped; add another to `capabilities` and every command picks it up.

## What is in the stack?

- **pnpm workspaces + Turborepo `2.11.2`** — cached `typecheck` / `test` / `build` across packages from day one.
- **Effect `4.0.0-rc.116`** — typed runtime, errors, filesystem service, and the three surfaces the capabilities project onto: `effect/unstable/cli`, `effect/unstable/httpapi` (with `OpenApi.fromApi`), and `effect/unstable/ai` (`Toolkit` + `McpServer`).
- **`@effect/platform-node` `4.0.0-rc.116`** — Node-backed services; keep adapter and core pins matched.
- **TypeScript `7.0.2`** — strict module and index-access checks.
- **XState `6.0.0-alpha.58`** — real lifecycle states; do not replace those with boolean soup.
- **`@xstate/effect` `0.1.0-alpha.2`** — the official XState v6 to Effect 4 bridge: `createEffectActor` runs a machine as a scoped Effect, `fromEffect` makes Effects into actors with typed failures and requirements. `packages/core/src/inspect-machine.ts` is the example.
- **Alchemy `2.0.0-beta.79`** — [Infrastructure as Effects](https://alchemy.run): `apps/infra/alchemy.run.ts` is the Stack; `pnpm infra:plan` / `infra:deploy` / `infra:destroy`; auth via `pnpm alchemy profile edit`.
- **Oxlint + Ultracite + Oxfmt** — native lint and format.
- **Vitest `5.0.1` + `@effect/vitest` `4.0.0-rc.116`** — `it.effect` and `it.layer` for every Effect test; running Effects by hand in a test file is a lint error.
- **`@effect/tsgo` `0.45.0`** — patches TypeScript 7 in `prepare` so Effect language-service diagnostics (leaked requirements, `any`/`unknown` in channels, global Date/fetch/console inside Effect, Node built-ins where Effect has a service) fail `tsc`.
- **varlock `1.20.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) via `./scripts/vendor-agent-sources.sh` (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. Upgrade pins as a reviewed stack change, not ambient drift.

## Commands

| Command | Purpose |
| --- | --- |
| `pnpm typecheck` | Check TypeScript without emitting files |
| `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 packages into `dist/` |
| `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

1. Rename workspace package names and the `bin` entry.
2. Rename the root command in `apps/cli/src/command.ts` and the version in `apps/cli/src/version.ts`.
3. Replace `inspectFile` in `packages/core` with one useful capability; everything in `capabilities` shows up on every surface you keep.
4. 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.
5. Keep expected failures typed and map them to deliberate exit codes.
6. Run `pnpm turbo run check test build` before the first push.

## Keep or cut

The template is itself a project, so it ships more than a bare scaffold. Delete what you will not use on day one; the fence will tell you what else has to go. After any cut: trim `packages/capability/src/index.ts`, run `pnpm install`, `pnpm fix`, then `pnpm turbo run check test build`, and update the package table in `AGENTS.md`.

| Want | Keep | Delete |
| --- | --- | --- |
| Only the CLI | `packages/capability/src/{capability,to-command}.ts` and their tests; all of `packages/core` | everything else in `packages/capability/src` and `test/`; `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 |  | `packages/capability/src/{catalog,sandbox,sandbox-error,to-code-mode}.ts` and `test/{catalog,sandbox,to-code-mode}.test.ts`; `codeMode` and `mcpServer.codeMode` in `surfaces.ts`; the `catalog` command and the `--code-mode` flag in `command.ts`; the code-mode and catalog cases in `cli.e2e.test.ts` |
| No HTTP |  | `packages/capability/src/to-http-api.ts` and its test; `http`, `routes`, and `webServer` in `surfaces.ts`; the `openapi` and `serve` commands; `apps/cli/test/serve.test.ts`; the openapi case in `cli.e2e.test.ts` |
| No MCP |  | `packages/capability/src/to-toolkit.ts`, its test, and `test/mcp-harness.ts`; `tools` and `mcpServer` in `surfaces.ts`; the `mcp` command; the MCP cases in `cli.e2e.test.ts`. Code mode imports from `to-toolkit.ts`, so cutting MCP cuts code mode too |
| No XState |  | `packages/core/src/inspect-machine.ts` and its test (call `FileInspector.inspect` directly from `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` |

`defineCapability` plus `toCommand` is the minimum that keeps `stats` working. `capability.ts` has no dependency on the other projections.

## License

MIT


---

# /vendor/README.md

Source: vendor/README.md
SHA-256: 1ebb8948125eae26692dc0c1ba4c0920f495cc9c6e9dff4b7416896565863c69

# Vendored packages

Temporary bridges for pinned dependencies that are not on npm yet. Each entry says how it was built and when it can go. Nothing is vendored right now.

| Tarball | Built from | Why | Remove when |
| ------- | ---------- | --- | ----------- |
| (none)  |            |     |             |

The last one was `xstate-effect-0.1.0-alpha.2.tgz`, built from the `statelyai/xstate` tag `xstate@6.0.0-alpha.58` with `pnpm exec preconstruct build && pnpm pack` after the first npm publish failed on 2026-09-17. npm published the same bytes on 2026-09-19 and `packages/core` moved to the registry version on 2026-09-20.

## Rules for the next one

- Add a `file:` spec in the consuming package, the tarball here, and a row above with the exact tag or commit it was built from. Build only from a pinned tag, never a moving branch, so the lockfile integrity stays reproducible.
- A `file:` tarball still goes through pnpm's release-age check, which looks the version up on the registry. An unpublished version 404s and fails `pnpm install --frozen-lockfile` in CI; a warm local metadata cache hides this. Every vendored tarball needs a matching version-scoped `minimumReleaseAgeExclude` entry in `pnpm-workspace.yaml`.
- When the version publishes, confirm the tarballs match (`npm pack <name>@<version>` and compare file lists and `dist`), replace the `file:` spec with the exact version, delete the tarball and its row, run `pnpm install`, and watch CI. Keep the `minimumReleaseAgeExclude` entry if the package is a prerelease pin like the others in that list.


---

# /pins.md

Source: workspace package.json files
SHA-256: 17e3bae5ee49dc731f9333ee5272f762e88f45a99bce011f6e36e434acbdfd04

# Workspace pins

Generated from every workspace `package.json`. The package files remain the source of truth.

## @rat-stack/cli

Source: `apps/cli/package.json`

| Package | Kind | Version |
| --- | --- | --- |
| `@effect/platform-node` | dependency | `4.0.0-rc.116` |
| `@effect/vitest` | devDependency | `4.0.0-rc.116` |
| `@rat-stack/capability` | dependency | `workspace:*` |
| `@rat-stack/core` | dependency | `workspace:*` |
| `@types/node` | devDependency | `24.13.6` |
| `effect` | dependency | `4.0.0-rc.116` |
| `typescript` | devDependency | `7.0.2` |
| `vitest` | devDependency | `5.0.1` |

## @rat-stack/infra

Source: `apps/infra/package.json`

| Package | Kind | Version |
| --- | --- | --- |
| `@effect/platform-node` | dependency | `4.0.0-rc.116` |
| `@rat-stack/mischief` | dependency | `workspace:*` |
| `@types/node` | devDependency | `24.13.6` |
| `alchemy` | dependency | `2.0.0-beta.79` |
| `effect` | dependency | `4.0.0-rc.116` |
| `typescript` | devDependency | `7.0.2` |

## @rat-stack/mischief

Source: `apps/mischief/package.json`

| Package | Kind | Version |
| --- | --- | --- |
| `@effect/vitest` | devDependency | `4.0.0-rc.116` |
| `@rat-stack/capability` | dependency | `workspace:*` |
| `@types/node` | devDependency | `24.13.6` |
| `alchemy` | dependency | `2.0.0-beta.79` |
| `effect` | dependency | `4.0.0-rc.116` |
| `typescript` | devDependency | `7.0.2` |
| `vitest` | devDependency | `5.0.1` |

## rat-stack

Source: `package.json`

| Package | Kind | Version |
| --- | --- | --- |
| `@earendil-works/pi-coding-agent` | devDependency | `0.86.1` |
| `@effect/tsgo` | devDependency | `0.45.0` |
| `@oxlint/plugins` | devDependency | `1.83.0` |
| `@types/node` | devDependency | `24.13.6` |
| `lefthook` | devDependency | `2.1.14` |
| `oxfmt` | devDependency | `0.68.0` |
| `oxlint` | devDependency | `1.83.0` |
| `oxlint-tsgolint` | devDependency | `7.0.2002` |
| `turbo` | devDependency | `2.11.2` |
| `typescript` | devDependency | `7.0.2` |
| `ultracite` | devDependency | `7.12.0` |
| `varlock` | devDependency | `1.20.0` |
| `vitest` | devDependency | `5.0.1` |

## @rat-stack/capability

Source: `packages/capability/package.json`

| Package | Kind | Version |
| --- | --- | --- |
| `@effect/platform-node` | devDependency | `4.0.0-rc.116` |
| `@effect/vitest` | devDependency | `4.0.0-rc.116` |
| `@types/node` | devDependency | `24.13.6` |
| `effect` | dependency | `4.0.0-rc.116` |
| `typescript` | devDependency | `7.0.2` |
| `vitest` | devDependency | `5.0.1` |

## @rat-stack/core

Source: `packages/core/package.json`

| Package | Kind | Version |
| --- | --- | --- |
| `@effect/platform-node` | dependency | `4.0.0-rc.116` |
| `@effect/vitest` | devDependency | `4.0.0-rc.116` |
| `@rat-stack/capability` | dependency | `workspace:*` |
| `@types/node` | devDependency | `24.13.6` |
| `@xstate/effect` | dependency | `0.1.0-alpha.2` |
| `effect` | dependency | `4.0.0-rc.116` |
| `typescript` | devDependency | `7.0.2` |
| `vitest` | devDependency | `5.0.1` |
| `xstate` | dependency | `6.0.0-alpha.58` |


---

# /resources/effect-4-reference-projects.svx

Source: .brain/resources/effect-4-reference-projects.svx
SHA-256: 49222a5e7016f5cfe941a33c5b5fdbcb0347f12f2c36345c98a2d8526fd6b025

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

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

## Projects

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

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

## Patterns every quality codebase shares

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

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

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

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

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

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

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

## Split, not consensus

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

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

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

## Sweep 2026-09-20

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

Two fence findings on the way:

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

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

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

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

## Sources

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


---

# /resources/schema-projections-and-code-mode.svx

Source: .brain/resources/schema-projections-and-code-mode.svx
SHA-256: 09d335ceebb7008d725f415207d06a6dde80cb6919324cbad15a557474aa4a74

---
title: One Schema, many surfaces (CLI, REST/OpenAPI, MCP, code mode)
type: resource
status: applied
created: 2026-09-18
updated: 2026-09-18
tags: [effect, mcp, openapi, cli, code-mode, executor, kody, architecture]
sources:
  - https://github.com/UsefulSoftwareCo/executor
  - https://github.com/kentcdodds/kody
  - https://blog.cloudflare.com/code-mode/
  - https://developers.cloudflare.com/agents/api-reference/codemode/
  - .agent_sources/github.com/Effect-TS/effect/packages/effect/src/unstable/{ai,cli,httpapi,rpc}
related:
  - ./effect-4-reference-projects.svx
---

# One Schema, many surfaces

Joel (2026-09-18): rat-stack is meant to build agent-first tools. The interesting
question is whether one Effect Schema definition can project into a CLI, a REST
API with OpenAPI, an MCP server, and a code-mode surface, and what Executor and
Kody teach about the last two.

## What Effect rc.115 already ships

Every target below is in `effect/unstable/*` today and takes `Schema` directly.
None of them talk to each other.

| Surface | Module | Entry | Schema goes in as |
| --- | --- | --- | --- |
| CLI | `unstable/cli` | `Command.make(name, config, handler)` | `Flag.withSchema`, `Flag.FileSchema`, `Argument.*` |
| REST + OpenAPI | `unstable/httpapi` | `HttpApi` / `HttpApiGroup` / `HttpApiEndpoint.{get,post,...}` | payload, success, error schemas |
| OpenAPI doc | `unstable/httpapi/OpenApi` | `OpenApi.fromApi(api)` | derived from the HttpApi |
| Typed client | `unstable/httpapi/HttpApiClient` | `HttpApiClient.make(api)` | derived from the HttpApi |
| MCP | `unstable/ai` | `Tool.make(name, { parameters, success, failure, dependencies, needsApproval })`, `Toolkit.make(...)`, `McpServer.toolkit(toolkit)`, `McpServer.layerStdio` / `layerHttp` | parameters, success, failure |
| JSON Schema | `unstable/ai/Tool` | `Tool.getJsonSchema(tool)`, `Tool.getJsonSchemaFromSchema(schema)` | any Schema |
| RPC | `unstable/rpc` | `Rpc.make`, `RpcGroup` | payload, success, error |

Tool annotations already exist as Context references: `Tool.Readonly`,
`Tool.Destructive`, `Tool.Idempotent`, `Tool.OpenWorld`. `needsApproval` is a
first-class option on `Tool.make`.

**The gap:** there is no `Toolkit.fromHttpApi`, no `Command.fromToolkit`, nothing
that lets one definition feed all four. That is the seam rat-stack can own.

## What the two reference repos do

### Executor (UsefulSoftwareCo, Effect 4)

- Integration layer, not a tool author: ingests MCP servers, OpenAPI, GraphQL,
  Google Discovery into one catalog, adds auth and per-tool policy
  (allow / approve / block), serves the catalog back over MCP.
- MCP surface: `execute` (code mode), `skills` (server-side guides for the
  model), `tools search` / `describe` / `call`, plus artifacts. The catalog is
  the product; code mode is one way to hold it.
- The kernel (`packages/kernel`) is the reusable idea. `ir/registry.ts`
  serializes a catalog as `{ version: "v4.1", types: Record<string, JsonSchema>,
  tools: [{ path, description, integrationId, input?, output?, error? }] }`.
  Live registrations carry `Schema.Top` for input/output/error; the serialized
  form carries JSON Schema names. Runtimes are pluggable: QuickJS, Deno
  subprocess, Cloudflare dynamic worker, workerd subprocess.
- Contract at the kernel edge is Standard Schema (`inputSchema: StandardSchema`),
  Effect Schema inside plugins. Effect in, JSON Schema at the wire.
- `execute-action` (artifact channel) is parsed against a one-call grammar
  instead of accepting arbitrary code. Good instinct: narrow the surface to what
  the producer can emit.

### Kody (Kent C. Dodds, Cloudflare Workers, Zod)

- Deliberately compact MCP surface: `search` (discovery over Vectorize plus
  lexical) and `execute` (sandboxed capability calls). "Do not add a new public
  MCP tool per capability."
- Authoring: `defineDomainCapability(domain, { name, description, inputSchema,
  outputSchema?, handler, tags?, keywords?, readOnly?, idempotent?,
  destructive?, requiredRole?, requiredPermission?, featureFlag? })`, grouped by
  `defineDomain`, flattened into a registry. Zod is normalized to JSON Schema
  for both the MCP description and the code-mode types.
- Same annotation vocabulary as Effect's `Tool` references, plus RBAC and flags.

### Cloudflare Code Mode (the origin)

One tool, `codemode({ code })`. The MCP or OpenAPI schema becomes a typed
TypeScript API with doc comments; the model writes TS against it; it runs in an
isolated Worker with no network, only bindings. Inside the sandbox:
`search(query)`, `describe(target)`, `step(name, fn)` (run once, replay), and
`run(snippet)`. Durable log, approval pause, rollback. The OpenAPI MCP server
variant exposes exactly two tools, `search` and `execute`.

## Proposed rat-stack shape

Keep Effect Schema as the single source and add one domain object plus
projections. Nothing here needs a new schema language.

```
                 Capability (Effect Schema in / out / error, Effect handler,
                             dependencies, annotations, approval)
                                          │
        ┌──────────────┬──────────────────┼──────────────────┬──────────────┐
        ▼              ▼                  ▼                  ▼              ▼
  Command.make   HttpApiEndpoint     Tool.make          catalog IR      (later)
  (unstable/cli) (unstable/httpapi)  Toolkit.make       JSON Schema +   Rpc.make
        │              │              McpServer.toolkit  generated d.ts
        ▼              ▼                  │                  ▼
     rat-stack     REST + OpenApi.fromApi │           search + execute
       CLI         + HttpApiClient        ▼           (two MCP tools,
                                     stdio / http      sandboxed runner)
```

- **Capability** is a `Schema`-typed record: `name`, `description`, `input`,
  `output`, `error`, `handler: (input) => Effect<Output, Error, Deps>`,
  annotations (`readOnly` / `destructive` / `idempotent` / `openWorld`),
  `needsApproval`. This is Kody's `defineDomainCapability` and Executor's
  `LiveToolRegistration` said in Effect.
- **Projections** are pure functions from a `Capability[]` (or a `Domain`) to
  the Effect surface: `toToolkit`, `toHttpApi`, `toCommand`. The handler's
  dependencies flow through unchanged, so the composition root stays one
  `Layer` as it is today in `apps/cli/src/cli.ts`.
- **Code mode** is a fourth projection, not a different system: serialize the
  catalog (Executor IR shape: JSON Schema per type, path per capability), emit
  `.d.ts` from the JSON Schema, expose `search` and `execute` as two ordinary
  `Tool`s whose `execute` handler runs model code in a sandbox that can only
  call capabilities by path. Executor's kernel proves the runtime is swappable;
  start with a subprocess, not a Worker.
- **Approval and policy** belong on the Capability, so all four surfaces agree
  on what is destructive. The CLI can prompt (`Prompt` in `unstable/cli`), MCP
  can use `needsApproval`, REST can return 202 with a resume token, code mode
  can pause like Cloudflare's runtime.

## Applied 2026-09-18 (slices 1 and 2)

- `packages/capability`: `defineCapability` and `toCommand`, `toHttpApi`, `toToolkit`. Twelve tests run each surface for real (CLI under `TestConsole`, HTTP via `HttpApiTest.groups`, MCP through an in-memory web handler with `RpcClient`).
- `packages/core`: `FileStats` became a Schema; `inspectFile` is the first capability; the lifecycle machine moved here from the CLI so every surface runs it.
- `apps/cli`: `stats` is `toCommand(inspectFile)`, plus `openapi`, `serve --port` (API, `/openapi.json`, Scalar at `/docs`), and `mcp` over stdio with logs on stderr. The e2e suite drives the built binary's MCP server over stdio and checks `readOnlyHint`.
- Type lessons. A heterogeneous list of capabilities needs a handler typed `(input: never) => Effect<unknown, unknown, unknown>` (contravariant parameters) and extractors that infer every type parameter at once. Schemas are bounded by `PlainSchema` (no decoding or encoding services) because projections decode at process edges. `HttpApiEndpoint.post` guards the error schema with a non-exported conditional type, so the runtime call goes through a loosely typed alias while `EndpointOf` names the precise type. The lint config bans namespaces and same-name redeclaration, so the API is plain named exports.
- Casts live at exactly two boundaries per projection (tuple-to-record and handler record), each with a reason; the two projection files turn off three language-service diagnostics that cannot tell a typed boundary from a leak.

## Applied 2026-09-18 (slices 3 and 4)

- `catalog.ts`: `toCatalog` (JSON Schema per channel via `Tool.getJsonSchemaFromSchema`, the same schemas MCP and OpenAPI publish), `toTypeScript` (a `.d.ts` for the `tools` object with `$defs` as named aliases and `@throws` on the failure), `searchCatalog` (token overlap over name, input fields, description; deterministic, no dependencies).
- `sandbox.ts`: a `Sandbox` service and `layerSubprocess`. The runner is passed with `node --permission --input-type=module -e <source>` so the permission model never has to allow a file read (`--allow-fs-read` needs the real path, which on macOS `/tmp` is not). File system, child processes, and workers are denied; `fetch` is not. Protocol is newline-delimited JSON over stdio through `ChildProcessSpawner`, a `Queue` feeding `handle.stdin`, and `Stream.decodeText` + `splitLines` on stdout.
- `to-code-mode.ts`: `search` and `execute` as two ordinary `Tool`s. `execute`'s description carries the generated declarations. Every `tools.<name>(input)` call decodes through the capability's input schema, runs the handler with the captured context, and encodes the output or the declared failure back; the program sees `InvalidInput`, `UnknownCapability`, or the failure's `_tag`.
- `apps/cli`: `mcp --code-mode` and `catalog [--types]`. The e2e suite runs a code-mode program against the built binary over stdio.
- Gotchas. `it.effect` runs on the `TestClock`, so a sandbox timeout never fires under it, and `test.live` inside an `it.layer` block behaved the same; the timeout test is a top-level `it.live` with the layer provided inline. A ternary over two differently typed `Layer`s cannot be piped further; pick the `Layer.launch` per branch instead. The `no-array-sort` lint rule plus an ES2022 lib left no legal way to sort; the workspace target and lib are ES2023 now.

## Template acceptance 2026-09-18

Scaffolded `joelhooks/rat-stack-scaffold-test` from the template (`gh repo create --template`), cloned it, and installed with a fresh pnpm store (`npm_config_store_dir` pointed at an empty directory). Install took 15.6s; corepack picked pnpm 11.3.0 from `packageManager` even with 11.10.0 on PATH. The `prepare` script patched tsc for `@effect/tsgo` and installed the lefthook hook. `pnpm turbo run check test build` passed: 14 tasks, 57 tests. `stats README.md`, `catalog --types`, and `pnpm env:check` all worked on the built output. Nothing needed fixing. The throwaway repo `joelhooks/rat-stack-scaffold-test` still exists (agent tooling blocks `gh repo delete`; Joel removes it). Rerun this after any dependency or vendor change; it is the only test of the template as a template.

## Seed

`inspectFile` (input `{ path }`, output `FileStats`, failure `FileStatsError`)
is the one capability shipped, projected to `stats`, `POST /inspectFile`, and
the `inspectFile` MCP tool. Slices 3 and 4 (catalog IR with generated types,
`search` + `execute`, subprocess sandbox) are next.

## Cautions

- Every module involved is `effect/unstable/*`. Pin and vendor as now; expect
  renames between rc's.
- `McpServer.layerHttp` requires `protocols: NonEmptyReadonlyArray<ProtocolAdapter>`
  and an `HttpRouter`; `layerStdio` is the cheap first target.
- Do not build the sandbox first. Three projections ship on top of shipped
  Effect modules; the sandbox is the only new runtime and should come after the
  catalog IR exists.
- JSON Schema is the wire type everywhere (Executor, Kody, Cloudflare). Keep
  Effect Schema inside the process and `Tool.getJsonSchemaFromSchema` at the
  edge; do not leak Effect types into the sandbox API.


---

# /skills/add-a-capability

Source: skills/add-a-capability/SKILL.md
SHA-256: 6237c9e29fde6bceaa4a3587fd7a7f10f1eea5b38f2c588ea26d57fc894803ad

---
name: add-a-capability
description: Add one schema-typed behavior to rat-stack and project it through CLI, HTTP, MCP, and code mode without duplicating handlers.
---

# Add a capability

Follow the existing `inspectFile` vertical slice. Do not write separate business logic for each surface.

## 1. Define the domain contract

In `packages/core/src/<capability>.ts`:

1. Define or import the output and failure schemas. Expected failures must be typed.
2. Call `defineCapability` from `@rat-stack/capability`.
3. Give it a stable name and short description.
4. Use a `Schema.Struct` input. Annotate fields with descriptions where useful.
5. Supply output and failure schemas, truthful annotations, and `needsApproval` when required.
6. Keep the handler thin. Call a domain service or lifecycle machine instead of putting transport logic in it.

```ts
export const doThing = defineCapability("doThing", {
  annotations: { idempotent: true, readOnly: true },
  description: "Do one concrete thing",
  failure: ThingError,
  handler: ({ id }) => ThingService.use((service) => service.run(id)),
  input: Schema.Struct({ id: Schema.String }),
  output: ThingResult,
});
```

Capability schemas must be plain: decoding and encoding cannot require services. Put dependencies in the handler's Effect requirement channel.

If the behavior needs a service, follow `packages/core/src/file-inspector.ts`: use a `Context.Service` class, capture dependencies in `make`, and keep `static layer` beside it. Export the capability and service from `packages/core/src/index.ts`.

## 2. Register it

In `packages/core/src/inspect-file.ts`, import the new capability and add it to the exported `capabilities` tuple:

```ts
import { doThing } from "./do-thing.js";

export const capabilities = [inspectFile, doThing] as const;
```

Keep catalog order intentional. This tuple feeds the HTTP, MCP, catalog, and code-mode projections in `apps/cli/src/surfaces.ts`.

## 3. Add the CLI projection

In `apps/cli/src/command.ts`, create a command with `toCommand(doThing, options)` and add it to `rootCommand`'s `Command.withSubcommands` list.

Use `name` when the public command name differs from the capability name. Use `positional` for selected input fields. Use `render` for human output; `toCommand` then adds `--json` automatically.

Do not hand-parse schema fields or call the service directly from the command.

## 4. Confirm every surface

No extra handler is needed for the other surfaces:

- `toHttpApi("RatStack", capabilities)` creates `POST /doThing` and includes it in OpenAPI.
- `toToolkit(capabilities)` exposes an MCP tool named `doThing` with the same schemas and annotations.
- `toCodeMode(capabilities)` adds it to catalog search and generated `tools.doThing(input)` declarations.
- Every code-mode call decodes input, runs the same handler, and encodes output or declared failure.

Provide any new service layer once in `apps/cli/src/cli.ts`, following `FileInspector.layer` and `NodeServices.layer`. The projections preserve handler requirements.

## 5. Test the vertical slice

Use `@effect/vitest`. Run Effects with `it.effect` or `it.layer`; do not call `Effect.run*` or `ManagedRuntime.make` in tests.

Add focused tests:

1. `packages/core/test/<capability>.test.ts`: run the handler through a test layer; assert encoded output, typed failure, and annotations.
2. `packages/capability/test/*`: change these only when projection behavior changes. Existing tests already cover CLI parsing, HTTP success/failure, MCP schemas/calls, catalog declarations/search, and sandbox execution.
3. `apps/cli/test/cli.e2e.test.ts`: run the built command; assert the OpenAPI path, MCP tool listing, and catalog declaration include the new capability; exercise code mode when the capability adds a meaningful execution path.

Use `Schema.encodeEffect` when asserting the wire shape. Use `Effect.flip` to inspect typed failures.

## 6. Verify

```sh
pnpm turbo run check test build
```

If the gate fails, fix the implementation. Do not loosen diagnostics, lint rules, hooks, or exact pins.


---

# /skills/add-a-lifecycle-machine

Source: skills/add-a-lifecycle-machine/SKILL.md
SHA-256: a0bf8e2b8460bd7f20d01ea0cc225b3a61a4dd80638f509acc588fc2651b5865

---
name: add-a-lifecycle-machine
description: Add an Effect-backed XState lifecycle for finite states, retries, cancellation, or resumable work in rat-stack.
---

# Add a lifecycle machine

Mirror `packages/core/src/inspect-machine.ts` when behavior has real modes or transitions. Do not replace a direct Effect unless the lifecycle matters.

Before editing Effect or XState code, read `node_modules/effect/AGENTS.md`, then inspect the pinned XState and `@xstate/effect` sources listed in `AGENTS.md`.

## 1. Name the lifecycle

Write the states and terminal outcomes first. Keep domain failures in an explicit outcome union when the machine must transition on them.

For file inspection, the machine owns `reading`, `inspected`, and `unreadable`. The terminal outcome is either `Inspected` with stats or `Unreadable` with `FileStatsError`.

## 2. Declare side-effect actors

Define each side effect with `fromEffect` outside the machine:

```ts
const performWork = fromEffect({
  effect: ({ input }) => ThingService.use((service) => service.run(input.id)),
  schemas: { input: Schema.Struct({ id: Schema.String }) },
});
```

Declared actors carry typed failures and service requirements. Only actions and actors declared through `setupEffect` contribute requirements to the Effect actor. Never return an Effect from an inline XState callback or spawn inline Effect logic.

## 3. Build with `setupEffect`

Use `setupEffect` with the declared actors and schemas, then call `createMachine`:

```ts
type ThingOutcome =
  | { readonly _tag: "Succeeded"; readonly value: ThingResult }
  | { readonly _tag: "Failed"; readonly error: ThingError };
interface ThingContext {
  readonly id: string;
  readonly outcome: ThingOutcome | undefined;
}

export const thingMachine = setupEffect({
  actors: { performWork },
  schemas: {
    context: types<ThingContext>(),
    input: Schema.Struct({ id: Schema.String }),
  },
}).createMachine({
  context: ({ input }) => ({ id: input.id, outcome: undefined }),
  initial: "working",
  output: ({ context }) => context.outcome,
  states: {
    working: {
      invoke: {
        src: "performWork",
        input: ({ context }) => ({ id: context.id }),
        onDone: {
          target: "succeeded",
          context: ({ context, event }) => ({
            ...context,
            outcome: { _tag: "Succeeded", value: event.output },
          }),
        },
        onError: {
          target: "failed",
          context: ({ context, event }) => ({
            ...context,
            outcome: { _tag: "Failed", error: event.error },
          }),
        },
      },
    },
    succeeded: { type: "final" },
    failed: { type: "final" },
  },
});
```

Let XState own states and transitions. Let Effect own side effects, typed errors, services, and resource scope.

## 4. Run it as an Effect

Start Effect-backed machines only with `createEffectActor`, never XState's `createActor`. Wait with `join` inside `Effect.scoped`:

```ts
export const runThingMachine = Effect.fn("runThingMachine")(function* (
  id: string
) {
  const actor = yield* createEffectActor(thingMachine, { input: { id } });
  // @effect-diagnostics-next-line anyUnknownInErrorContext:off
  const outcome = yield* join(actor).pipe(Effect.orDie);
  if (outcome === undefined) {
    return yield* Effect.die(new Error("machine completed without an outcome"));
  }
  if (outcome._tag === "Failed") {
    return yield* outcome.error;
  }
  return outcome.value;
}, Effect.scoped);
```

A machine-level error or early stop is a defect in this shape. The domain failure travels through a final state. `join` has an `unknown` machine error channel, so the reference uses a targeted `anyUnknownInErrorContext` diagnostic override and `Effect.orDie`.

Call the runner from the capability handler, as `packages/core/src/inspect-file.ts` does. Provide the actor's service layer at the composition root in `apps/cli/src/cli.ts`.

## 5. Test both terminal paths

Use `@effect/vitest` with `it.layer`. Start the machine with `createEffectActor`, `join` it, then assert both the final snapshot state and output. Also test that the runner returns success and lifts the domain failure into Effect's error channel.

The `xstate-effect/no-inline-effect` rule in `scripts/oxlint-plugin-xstate-effect.ts`, wired through `oxlint.config.ts`, guards the declared-actor boundary. Do not disable it to make inline Effect logic pass.

## 6. Verify

```sh
pnpm turbo run check test build
```


---

# /skills/keep-or-cut

Source: skills/keep-or-cut/SKILL.md
SHA-256: a766e47332c8f20c63ae80decfcc9e5659257b174a655e10c5918a1d51b2e0e6

---
name: keep-or-cut
description: Remove unused rat-stack surfaces from a new clone while preserving the smallest working capability-to-CLI scaffold and its fence.
---

# Keep or cut rat-stack

Use this on day one of a clone. Delete surfaces you will not use; do not keep speculative machinery.

Read `AGENTS.md` first. Preserve unrelated product work. Inspect imports and tests before deleting files.

## Minimum: CLI only

The smallest working slice is:

- `packages/capability/src/capability.ts`
- `packages/capability/src/to-command.ts`
- their tests
- all of `packages/core`
- the CLI composition needed by the projected command

`defineCapability` plus `toCommand` keeps the shipped `stats` command working. `capability.ts` does not depend on the other projections.

For CLI only, delete:

- every other file and matching test in `packages/capability/src` and `packages/capability/test`
- `apps/cli/src/surfaces.ts`
- `catalog`, `openapi`, `serve`, and `mcp` commands from `apps/cli/src/command.ts`
- `apps/cli/test/serve.test.ts`
- catalog, OpenAPI, MCP, and code-mode cases from `apps/cli/test/cli.e2e.test.ts`

## Cut 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`
- `packages/capability/src/to-code-mode.ts`
- `packages/capability/test/catalog.test.ts`
- `packages/capability/test/sandbox.test.ts`
- `packages/capability/test/to-code-mode.test.ts`
- `codeMode` and `mcpServer.codeMode` from `apps/cli/src/surfaces.ts`
- the `catalog` command and `--code-mode` flag from `apps/cli/src/command.ts`
- catalog and code-mode cases from `apps/cli/test/cli.e2e.test.ts`
- the `./sandbox` and `./code-mode` exports from `packages/capability/package.json`

## Cut HTTP

Delete:

- `packages/capability/src/to-http-api.ts`, `packages/capability/src/http-api.ts`, and the projection test
- the `./http-api` export from `packages/capability/package.json`
- `http`, `routes`, and `webServer` from `apps/cli/src/surfaces.ts`
- the `openapi` and `serve` commands from `apps/cli/src/command.ts`
- `apps/cli/test/serve.test.ts`
- the OpenAPI case from `apps/cli/test/cli.e2e.test.ts`

## Cut MCP

Delete:

- `packages/capability/src/to-toolkit.ts`, `packages/capability/src/toolkit.ts`, and the projection test
- `packages/capability/test/mcp-harness.ts`
- `tools` and `mcpServer` from `apps/cli/src/surfaces.ts`
- the `mcp` command from `apps/cli/src/command.ts`
- MCP cases from `apps/cli/test/cli.e2e.test.ts`
- the `./toolkit` export from `packages/capability/package.json`

Code mode imports `to-toolkit.ts`. Cutting MCP therefore cuts code mode too; apply both lists.

## Cut XState

Delete:

- `packages/core/src/inspect-machine.ts` and its test
- `xstate` and `@xstate/effect` from `packages/core/package.json`
- their `minimumReleaseAgeExclude` entries from `pnpm-workspace.yaml`
- `scripts/oxlint-plugin-xstate-effect.ts`
- its entry in `oxlint.config.ts`

Then call `FileInspector.inspect` directly from the capability handler in `packages/core/src/inspect-file.ts`.

## Clean every cut

1. Remove stale exports from `packages/capability/src/index.ts` and any affected package barrel.
2. Remove stale imports, layers, commands, and tests found by the compiler.
3. Update the package table in `AGENTS.md` so repo law matches the clone.
4. Refresh the lockfile and format intentional changes:

```sh
pnpm install
pnpm fix
pnpm turbo run check test build
```

Let the fence expose every dangling reference. Do not silence diagnostics or delete tests unrelated to the removed surface.


---

# /skills/learn-rat-stack

Source: skills/learn-rat-stack/SKILL.md
SHA-256: 8c6b01b9a4e5122bdcc3fbef4d82bea410ca6c1388a6e2c8ba14dd488c9edc46

---
name: learn-rat-stack
description: Understand rat-stack before changing a clone, choosing a surface, adding behavior, or trimming the template.
---

# Learn rat-stack

Use this skill when you enter the repo or need to decide where a change belongs.

## Read first

1. Read `AGENTS.md`. It is repo law: pins, architecture, commands, boundaries, and stop rules.
2. Read `VISION.md`. It explains why the scaffold exists and which outcomes matter.
3. Read `README.md` for the current workspace and runnable example.
4. Before Effect or XState work, read `node_modules/effect/AGENTS.md` and the pinned mirrors listed in `AGENTS.md`.

A product clone should replace rat-stack's product intent and project-law sections instead of stretching them.

## The core rule

Define behavior once as a `Capability`:

- Effect Schema for input, output, and declared failure
- one Effect handler
- read-only, destructive, idempotent, and open-world annotations
- an approval flag

`packages/capability/src/capability.ts` defines that object. Domain capabilities live in `packages/core`. The `capabilities` tuple in `packages/core/src/inspect-file.ts` is the registry.

Four projections expose the same contract:

1. CLI: `toCommand` maps schema fields to arguments and flags.
2. HTTP: `toHttpApi` creates `POST /<capability>` endpoints and derived OpenAPI.
3. MCP: `toToolkit` creates one MCP tool per capability.
4. Code mode: `toCodeMode` creates `search` and `execute`; sandbox calls still pass through capability schemas and handlers.

`apps/cli/src/surfaces.ts` instantiates HTTP, MCP, and code mode from the registry. `apps/cli/src/command.ts` creates CLI commands. `apps/cli/src/cli.ts` is the composition root that provides service layers.

## Choose the next skill

- Add domain behavior: use `add-a-capability`.
- Add finite states, retries, cancellation, or another lifecycle: use `add-a-lifecycle-machine`.
- Remove unused surfaces from a clone: use `keep-or-cut`.

Do not bypass a capability with a custom route, command, MCP handler, or sandbox function. Schemas are the contract, and generated OpenAPI, MCP schemas, catalog JSON Schema, and code-mode declarations derive from them.

## Finish

Run the required gate:

```sh
pnpm turbo run check test build
```

Do not weaken the fence to make the gate pass.
