Learn the stack

Start with inspectFile, a capability27: one named action with a contract and a server-side handler. The contract27 defines its name, input, output, failure, and metadata. The handler implements the action.

A service is a named interface for one job. A machine defines states and the events that permit movement between them. A projection18 builds a callable interface from capabilities.

Follow these parts, then try the hosted content tools.

Read Effect basics29 before this trace if the computation model is new. For implementation tasks, use services26, Layer composition18, and testing22.

The pieces

One action, five interfaces

                     ┌─ CLI
                     ├─ HTTP
One Capability ──────┼─ MCP
                     ├─ code mode
                     └─ RPC

A projection18 exposes the same action through another interface. RPC serves browser clients. Code mode21 lets agents compose calls in one program.

All five interfaces use one handler.

Trace one action

The CLI uses inspectFile. The hosted site uses a separate content registry.

1. Define the contract

The contract27 names the action and its input, output, and failure schemas.

packages/core/src/contracts.ts · b4d6b8f4
export const inspectFileContract = defineContract("inspectFile", {
  annotations: { idempotent: true, readOnly: true },
  description: "Count bytes, characters, words, and lines in a file",
  failure: FileStatsError,
  input: Schema.Struct({
    path: Schema.String.annotate({ description: "File to inspect" }),
  }),
  output: FileStatsSchema,
});

FileStatsError belongs to the interface. The annotations mark a repeatable, read-only action.

2. Bind one handler

implement binds the contract to runInspectMachine. The registry includes the resulting capability27.

packages/core/src/inspect-file.ts · b4d6b8f4
export const inspectFile = implement(inspectFileContract, ({ path }) =>
  runInspectMachine(path)
);

export const capabilities = [
  inspectFile,
  ...flagCapabilities,
  joinInterest,
  listPrompts,
  getPrompt,
  listCafeNews,
  listCafeProjects,
] as const;

The handler starts a machine to read the file.

3. Put file work in a service

FileInspector.make captures the file system. Its inspect method reads bytes and maps file errors to FileStatsError.

packages/core/src/file-inspector.ts · b4d6b8f4
  make: Effect.gen(function* makeFileInspector() {
    const fileSystem = yield* FileSystem.FileSystem;

    const inspect = Effect.fn("FileInspector.inspect")(function* inspect(
      path: string
    ) {
      const content = yield* fileSystem
        .readFile(path)
        .pipe(
          Effect.mapError(
            (error) => new FileStatsError({ path, reason: error.message })
          )
        );

      return summarizeBytes(path, content);
    });

The service exposes its Layer12 beside the implementation. This follows hexagonal architecture27: composition supplies the file-system dependency.

packages/core/src/file-inspector.ts · b4d6b8f4
    return { inspect } as const;
  }),
}) {
  static readonly layer = Layer.effect(this, this.make);
}

Callers ask the service to inspect a path. Composition chooses the file-system implementation.

4. Give the lifecycle named states

A declared createEffectLogic actor calls FileInspector.

packages/core/src/inspect-machine.ts · b4d6b8f4
const readStats = createEffectLogic({
  effect: ({ input }) =>
    FileInspector.use((inspector) => inspector.inspect(input.path)),
  schemas: { input: Schema.Struct({ path: Schema.String }) },
});

The machine starts in reading. It finishes in inspected or unreadable.

packages/core/src/inspect-machine.ts · b4d6b8f4
  initial: "reading",
  output: ({ context }) => context.outcome,
  states: {
    inspected: { type: "final" },
    reading: {
      invoke: {
        input: ({ context }) => ({ path: context.path }),
        onDone: {
          context: ({ event }) => ({
            outcome: inspectOutcome.Inspected({ stats: event.output }),
          }),
          target: "inspected",
        },
        onError: {
          context: ({ event }) => ({
            outcome: inspectOutcome.Unreadable({ error: event.error }),
          }),
          target: "unreadable",
        },
        src: "readStats",
      },
    },
    unreadable: { type: "final" },
  },
});

5. Project the registered actions

The command-line interface (CLI) projects capability lists for each server interface. HTTP uses stateless learning capabilities. MCP and code mode21 use their local learning counterparts. All three include inspectFile.

apps/cli/src/surfaces.ts · b4d6b8f4
export const capabilities = [
  ...coreCapabilities,
  ...learnCapabilities,
] as const;

export const cliCapabilities = [
  ...coreCapabilities,
  ...localLearnCapabilities,
] as const;
export const http = toHttpApi("RatStack", capabilities);

export const tools = toToolkit(cliCapabilities);

export const codeMode = toCodeMode(cliCapabilities);

export const routes = Layer.merge(
  HttpApiBuilder.layer(http.api, { openapiPath: "/openapi.json" }).pipe(
    Layer.provide(http.layer)
  ),
  HttpApiScalar.layer(http.api, { path: "/docs" })
);

The RPC projection18 derives contracts from its supplied capabilities.

packages/capability/src/to-rpc.ts · b4d6b8f4
export const toRpc = <const Caps extends readonly AnyCapability[]>(
  capabilities: Caps
): RpcProjection<Caps> => {
  const contracts = capabilities.map((capability) => capability.contract);

A projection takes a registry and reuses its domain handlers.

6. Connect the CLI

toCommand derives each command. inspectFile also gets the stats alias.

apps/cli/src/command.ts · b4d6b8f4
const capabilityCommands = commandCapabilities.map((capability) => {
  const command =
    capability === inspectFile
      ? toCommand(capability, {
          positional: ["path"],
          render: formatFileStats,
        }).pipe(Command.withAlias("stats"))
      : toCommand(
          capability,
          capability === setFlag ? { promptForApproval: true } : undefined
        );

  return command;
});

The entry point supplies the service Layer12 and Node services. It denies approval27 by default.

apps/cli/src/cli.ts · b4d6b8f4
  Effect.provide(
    Layer.mergeAll(
      Layer.provideMerge(FileInspector.layer, NodeServices.layer),
      remoteJoinInterestLayer,
      localPromptLibraryLayer.pipe(Layer.provide(NodeServices.layer)),
      localCafeDirectoryLayer.pipe(Layer.provide(NodeServices.layer)),
      localLearnLayer,
      Flags.defaults(eventFlags),
      Approval.denyAll
    )
  )

The command keeps the shared handler.

7. Compose the cloud

An Alchemy Stack groups one cloud infrastructure program and its outputs. Alchemy declares the Stack, Cloudflare providers, and state. The Stack includes Mischief and Website.

apps/infra/alchemy.run.ts · b4d6b8f4
export default Alchemy.Stack(
  "RatStack",
  {
    providers: Layer.mergeAll(Cloudflare.providers(), Drizzle.providers()),
    state: Cloudflare.state(),
  },
  Effect.gen(function* stack() {
    const website = yield* Website;

    const mischief = yield* Mischief;

    yield* mischief.bind`${website}`({
      bindings: [
        { name: "WEBSITE", service: website.workerName, type: "service" },
      ],
    });

    return { mischiefUrl: mischief.url, websiteUrl: website.url };
  }).pipe(

The Stack deploys the cloud applications. The Node file-inspection CLI runs separately.

Try hosted search and read

Use POST /api/execute for code mode21. The agent guide documents this search → read program:

const found = await tools.search({ query: "cartridges", limit: 1 });
const page = await tools.read({ id: found.matches[0].id });
return { title: page.title, id: page.id };

Send it as a JSON code string:

curl --request POST 'https://ratstack.sh/api/execute' \
  --header 'content-type: application/json' \
  --data '{"code":"const found = await tools.search({ query: \"cartridges\", limit: 1 }); const page = await tools.read({ id: found.matches[0].id }); return { title: page.title, id: page.id };"}'

read takes the exact ID from search. This hosted program reads site content.

Read before changing a piece

  1. Read AGENTS.md for pins, boundaries, and the fence45.
  2. Read VISION.md for intent.
  3. Read the package you plan to change.
  4. Before Effect or XState work, read node_modules/effect/AGENTS.md and the pinned sources listed in repo law.

In a copied product repo, replace the template’s product notes and rules. Keep the stack lessons that still serve the product.

Follow repo law and pinned code when an example is stale.

Pick the next skill

Finish

Run the full gate:

pnpm turbo run check test build

Fix failures. Keep the fence45 intact.

Check deployment separately from the gate.


Last changed 2026-10-18 in e5c3077. Source on GitHub. Change log.

Unlinked mentions (17)

Content updated .