Skip to content

Repository files navigation

otter

otter

A light software factory for Effect.ts monorepos
with agent-friendly tooling for code quality, documentation, and work tracking.


Philosophy

Explicit control flow. Every branch handled, every error typed. Effect makes failure cases visible in function signatures — TaggedError gives errors identity, catchTag forces you to handle them by name. No silent Effect.catchAll recoveries, no throw inside Effect.gen, no bare new Error. You can read any function and know exactly what can go wrong.

Code shape enforcement. ast-grep rules enforce selected architecture cases, not just style. Tagged errors must live in errors.ts. Boundary conventions say external SDK wrappers and non-Effect I/O live in adapter files, while injected Effect platform services can be used in interior code. Current rules catch selected boundary anti-patterns, including direct node:fs imports and Effect.runPromise / runSync in Effect code. See the rule summary below.

Runtime observability. Structured logging with span context and templates that expect spans at boundaries. When tracing and logging layers are wired, set EFFECT_TRACE=1 to emit console traces and correlated structured logs. The conventions establish the preconditions: Effect services are injectable, log calls use structured annotations, and boundary templates show where spans belong. See docs/patterns/observability.md and docs/patterns/boundaries.md.

Getting started

Install the prerequisites, then:

bun install

Start your coding agent in this repo and start building. See AGENTS.md for the full command reference, enforcement rules, and conventions.

Tools

ToolRole
bunPackage manager and runtime
oxlint + oxfmtLinting and formatting
tsgoTypeScript native compiler (preview)
ast-grepCustom TypeScript lint rules (Effect-specific patterns)
driftBinds markdown docs/scenarios to source or template targets
fpLocal-first issue tracking with lifecycle extensions

Structure

apps/ Deployable applications (CLIs, APIs, workers)
packages/ Internal shared packages
packages/qa/ Prose-first QA scenarios, helpers, and local run results
docs/ Conventions, templates, architecture notes, proposals, experiments
rules/ ast-grep lint rules (shared + Effect-specific)
.fp/extensions/ fp lifecycle extensions

QA scenarios

packages/qa provides a prose-first scenario surface for validating scaffolded CLIs, APIs, workers, and browser-visible flows. Scenarios are markdown files with small YAML frontmatter, reusable helper docs, gitignored local results, and explicit drift bindings to the templates or code they exercise.

Start with packages/qa/README.md for the authoring guide and examples, and docs/testing/qa.md for when to use QA scenarios versus code-level tests.

ast-grep rules

Custom lint rules in rules/, run via bun run lint:ast.

Shared (all TypeScript):

RuleWhat it catches
no-dynamic-importDynamic import() — use static imports
no-else-after-returnUnnecessary else after return — use early returns
no-foreach.forEach() — use for...of
no-hardcoded-colorsHardcoded color literals in TS/TSX

Effect (apps and packages):

RuleWhat it catches
no-bare-new-errornew Error(), new TypeError(), etc. — use TaggedError or let unknowns propagate
no-console-logconsole.* — use Effect.log
no-direct-fs-importDirect node:fs imports — use Effect's FileSystem service
no-fetch-in-effectEffect.tryPromise wrapping fetch() — use @effect/platform's HttpClient
no-interface-in-modelsexport interface in models — use Schema.Struct
no-interpolated-loggingTemplate literals or concatenation in log calls — use structured annotations
no-json-parse-without-schemaBare JSON.parse — validate parsed data through Schema
no-manual-json-decodeEffect.try({ try: () => JSON.parse(...) }) — use Schema.parseJson(Inner)
no-manual-tag-checkManual ._tag checks — use Effect.catchTag or Match.tag
no-runpromise-in-effectEffect.runPromise/runSync inside Effect code — use yield* or boundary pattern
no-silent-catchEffect.catchAll without logging — always log before recovering
no-throw-in-effect-generatorthrow in Effect generators — use Effect.fail
no-try-catch-in-effecttry/catch in Effect code — use Effect.try or Effect.catchTag
no-typed-boundary-assignmentTyped assignment from parsed boundary data — decode first
no-unsafe-typecast-at-boundaryas casts on boundary data — decode with Schema
tagged-error-locationData.TaggedError outside errors.ts — keep error definitions co-located
use-tagged-errorclass X extends Error — use Data.TaggedError

fp extensions

Extensions in .fp/extensions/ hook into fp's issue lifecycle to enforce workflow quality.

auto-done

Manages parent/child issue lifecycle automatically.

  • Pre-hook: blocks marking a parent issue as done if any children are still open
  • Post-hook: when the last child is marked done, auto-marks the parent done

check-before-done

Can gate the done transition on passing checks.

This repo currently disables the gate with checks = "". Set checks = "bun run check" to run oxlint, ast-grep, drift, and typecheck before allowing an issue to move to done:

[extensions.check-before-done]
checks = "bun run check"# comma-separated commands

done-reminder

Prints a reminder to stderr when an issue transitions to done, prompting the agent to:

  • Run code review (via subagent) if the work was non-trivial
  • Update docs/ with architectural or flow decisions, using the drift skill to link specs to relevant source files

Prerequisites

bun

curl -fsSL https://bun.sh/install | bash

See bun.sh for more options.

fp

curl -fsSL https://setup.fp.dev/install.sh | sh -s

See fp.dev for more info.

drift

curl -fsSL https://drift.fp.dev/install.sh | sh

See github.com/fiberplane/drift for more info.

About

No description, website, or topics provided.

Resources

Stars

78 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages