Code Style

General coding conventions for backend and frontend. UI palette and primitive reference live in frontend.md. The why behind these rules — the durable design principles they implement — lives in docs/engineering-principles.md.

Type Checking and Linting

Run checks after writing code. The build tool (esbuild) does NOT do type checking or linting — it just strips types. A pre-commit hook runs both automatically.

pnpm typecheck    # TypeScript errors
pnpm lint:changed # ESLint over your diff (iteration); `pnpm lint` is the whole tree
pnpm lint:oxlint  # Supplemental linter (fast, catches patterns ESLint misses)
pnpm lint:knip    # Dead code detector — run from the MONOREPO ROOT (see docs/maintenance.md)
pnpm lint:circular  # Circular dependency detector (madge)

Error Handling

Logging levels

One policy for console.*, so a level carries meaning:

Existing call sites migrate opportunistically as you touch them, not in a sweep.

Async error handling

Exhaustiveness

Every dispatch over a closed union must fail to compile when a member is added — never rely on a default: to swallow the new case.

Defensiveness

Right-sized: defense concentrates at real boundaries; interior code trusts its types. Before adding a check, ask what produced the value.

  1. Untrusted-boundary data (disk/YAML/HTTP/subprocess/LLM) may default, narrow, or catch-with-fallback freely — prefer safeParse where a schema exists.
  2. A default may only replace a value the type system says can be absent. ?? x on a required field is a type-system lie — fix the type instead.
  3. Every non-rethrowing catch logs, or carries a one-line justification comment (the /* ignore: <reason> */ convention).
  4. UI polling is not exempt — a silent .catch(() => {}) on a poll turns a dead backend into a frozen UI. Retry-resilience and observability are different properties; log even when the poll retries.
  5. User-initiated actions never silently no-op: a toast/inline error, or at minimum a logged error.
  6. Discriminated-union dispatch uses assertNever, never an invented default fallback.
  7. Process-supervision code keeps the biggest defensive budget — each catch commented with the race it absorbs (workstreams-app/src/router/router.ts is the model).
  8. Before adding a check, ask what produced the value: same-repo typed code → an assertion or nothing; disk/network/another process → keep the check.

Lint rule suppression

Every rule in @ianbicking/personal-vibe-check is a deliberate choice, and the preset is ours to extend. Suppression is sometimes right, under strict limits:

Code Style