Skip to Content
DocsContributingCode conventions

Code conventions

This page is the quick reference for contributors hacking on the klera codebase itself. It is not a guide for adopters; if you’re authoring flows, head over to authoring prose flows.

The repo’s CLAUDE.md is the canonical source — when in doubt, read that first. The rules below are the subset that comes up on every PR.

Workspace layout

The repo is a pnpm workspace with eleven packages under packages/*, plus an Expo demo at examples/expo-demo that lives outside the pnpm workspace and uses plain npm. Run cd examples/expo-demo && npm install once after cloning; pnpm -w commands do not cover it.

vitest.config.ts aliases @klera/* directly to each package’s src/index.ts, so tests run against source without a build step. Don’t run pnpm build just to make tests see a change.

Language

TypeScript, strict mode. Three flags are non-negotiable:

  • exactOptionalPropertyTypes
  • noUncheckedIndexedAccess
  • verbatimModuleSyntax

Don’t fight them. If something feels awkward — a type predicate that won’t narrow, an array access that won’t compile without a guard — the fix is almost always to be more precise about what you mean, not to loosen the flag.

Modules

ESM throughout. Imports use the .js extension even for .ts sources (TypeScript ESM convention):

import { runFlow } from './runner.js';

Workspace packages depend on each other through src/index.ts exports onlyimport/no-internal-modules enforces this. If you need to reach into another package’s internals, the right move is to widen the exporting package’s surface, not to deep-import.

Validation

Parse at trust boundaries with Zod. Never as SomeType across a boundary.

// Wrong: cast across a boundary. const flow = JSON.parse(raw) as Flow; // Right: validate at the boundary. const flow = Flow.parse(JSON.parse(raw));

The boundaries that matter today are: YAML / Markdown flow files, WebSocket messages between runtime and bridge, MCP tool inputs, planner LLM responses, on-disk report JSON. Each has a Zod schema in @klera/protocol; reach for safeParse when you want a structured error and parse when “the file is corrupt” is the only sensible response.

Async

async/await everywhere. No floating promises — @typescript-eslint/no-floating-promises enforces this. If you’re firing-and-forgetting on purpose, mark it explicitly:

void backgroundFlush(); // intentional

Logging

Route through a per-package logger. console.log is forbidden in shipped code; the lint rule will catch it. console.warn and console.error are allowed for library fallbacks where a real logger isn’t available yet (e.g. early bootstrap before the logger is constructed).

import { logger } from './logger.js'; logger.info('flow.start', { flow: name }); logger.error('matcher.exhausted', { target, attempts });

Adopter-side log routing happens through the OTel correlated logger described in observability; within the codebase you only need to call the package logger.

Tests

Vitest. Co-locate *.test.ts next to the source file. Target ≥80% coverage (enforced in vitest.config.ts). Test names read like sentences:

test('self-heals when the parent re-keys', async () => { ... });

Running a single test

pnpm test packages/engine/src/matcher.test.ts

For local iteration, pnpm test:watch runs vitest in watch mode.

Lint, format, filenames, commits

  • Lint: @typescript-eslint/strict-type-checked. Don’t silence a rule without a comment explaining why.
  • Format: Prettier, repo config. CI runs pnpm format:check.
  • File names: kebab-case. unicorn/filename-case enforces this.
  • Commit messages: Conventional Commits (feat:, fix:, docs:, chore:, refactor:, test:). One logical change per commit.
feat(engine): add waitForIdle network gate fix(matcher): self-heal across re-keyed parents docs(observability): document OTLP backfill

Working on a new package? Add a folder under packages/ with package.json, tsconfig.json, tsup.config.ts, src/index.ts. Name it @klera/<short-name>. Add a project reference in the root tsconfig.json. Add a design note under docs/decisions/ if the package introduces a new concept or boundary.

Verification

Before declaring work done, run the full gate. CI runs the same commands; if they pass locally, they pass on PR.

Install once after pulling

pnpm install

Build every package

pnpm build

Fans out across the workspace with -r --parallel.

Typecheck

pnpm typecheck

Tests

pnpm test

Lint

pnpm lint

Format

pnpm format:check

Local iteration helpers (not part of the verification gate):

  • pnpm test:watch — vitest in watch mode.
  • pnpm lint:fix — autofix lint where possible.
  • pnpm format — rewrite files with prettier.

If anything fails, fix it before handing back. “It probably works” is not sufficient.

When in doubt

Prefer deleting to adding. Prefer derived state to primary state. Prefer failing loudly to failing quietly. If you can’t decide between two options, write a short design note in docs/decisions/ — the act of writing usually makes the answer obvious.

Next: releasing for how a merge to main becomes a published version on npm.

Last updated on