Joel's agentic scaffold for an Effect app. VISION.md explains why. AGENTS.md defines the fence through exact pins, checks, and hooks. The public tree is for stealing 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.
gh repo create <name> --template joelhooks/rat-stackClone the new repository, then install and verify it:
nvm use
pnpm install
pnpm vendor:agent-sources
pnpm turbo run check test buildNode 24.18.0 and pnpm 11.3.0 are required. The requirement is declared in .nvmrc, engines, devEngines, and packageManager.
| 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) |
apps/mischief |
@rat-stack/mischief |
Cloudflare Worker for the public site, agent discovery, and sandboxed execute |
.agent_sources/ |
β | Shallow upstream mirrors (gitignored clones; see README there) |
The included stats command counts bytes, Unicode characters, words, and lines in a file:
pnpm cli stats README.md
pnpm cli stats README.md --jsonAfter pnpm build, you can run the built entrypoint directly:
node apps/cli/dist/cli.js stats README.mdThe same capability on the other surfaces:
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 seesCode 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, then verify every projection you keep; CLI command registration lives in apps/cli/src/command.ts.
- pnpm workspaces + Turborepo
2.11.2: cachedtypecheck/test/buildacross 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(withOpenApi.fromApi), andeffect/unstable/ai(Toolkit+McpServer). @effect/platform-node4.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/effect0.1.0-alpha.2: the official XState v6 to Effect 4 bridge:createEffectActorruns a machine as a scoped Effect,fromEffectmakes Effects into actors with typed failures and requirements.packages/core/src/inspect-machine.tsis the example.- Alchemy
2.0.0-beta.79: Infrastructure as Effects:apps/infra/alchemy.run.tsis the Stack;pnpm infra:plan/infra:deploy/infra:destroy; auth viapnpm alchemy profile edit. - Oxlint + Ultracite + Oxfmt: native lint and format.
- Vitest
5.0.1+@effect/vitest4.0.0-rc.116:it.effectandit.layerfor every Effect test; running Effects by hand in a test file is a lint error. @effect/tsgo0.45.0: patches TypeScript 7 inprepareso Effect language-service diagnostics (leaked requirements,any/unknownin channels, global Date/fetch/console inside Effect, Node built-ins where Effect has a service) failtsc.- varlock
1.20.0:.env.schemadeclares every variable with@env-specdecorators;pnpm checkrunsvarlock load, secrets stay in gitignored.env.local. - Vendored agent sources: Effect, effect-solutions, XState, 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.
| 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 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 |
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/.
Start with the template:
gh repo create <name> --template joelhooks/rat-stackClone the new repository, then install it:
pnpm installRename the workspace package names before you build on the example. The current name fields are:
package.json:rat-stackapps/cli/package.json:@rat-stack/cliapps/infra/package.json:@rat-stack/infraapps/mischief/package.json:@rat-stack/mischiefpackages/capability/package.json:@rat-stack/capabilitypackages/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/mischief. Find them with:
rg -n '@rat-stack/' --glob '*.json' --glob '*.ts' -lA new scope changes line lengths, so run the formatter once after the rename:
pnpm fixReplace 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/mischief 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/mischief/src/worker.ts. Choose an Alchemy stage for each plan or deploy. The stage is a command-line choice, for example --stage <stage>. Configure credentials with:
pnpm alchemy profile edit --add CloudflareRun the acceptance gate before the first push:
pnpm turbo run check test build- 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, andapps/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, andpackages/capability/test/to-toolkit.test.tscheck derived JSON Schema and typed failures. - A fresh sandbox isolates model code. Proof:
packages/capability/test/sandbox.test.tschecks file-system and child-process denial plus timeout handling. - Rate limits fail closed. Proof:
apps/mischief/test/worker.test.tschecks 429 responses before another execute worker starts and when the global limit denies. - Markdown is the default machine representation. Proof:
apps/mischief/test/worker.test.tschecks Markdown withoutAccept: text/htmland HTML only when requested.
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.ts, packages/capability/src/to-command.ts, and their tests; all of packages/core |
every other file in packages/capability/src and packages/capability/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.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, 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 | 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 | 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 |
|
| No XState | 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 |
defineCapability plus toCommand is the minimum that keeps stats working. packages/capability/src/capability.ts has no dependency on the other projections.
MIT