This repository has been archived on 2026-06-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
nerve/CLAUDE.md

6.0 KiB

Nerve Coding Conventions

Core Concepts

External World → Sense → Signal → Workflow → Log
                  ↑                                     ↑
              "what to observe"                  "what to do"

Nerve is a lightweight observation engine daemon for autonomous agents. It continuously observes external state, reacts to changes via declarative rules, and orchestrates multi-step workflows.

Key Terms

Concept What it is
Sense A compute() function that samples or derives data. Returns ComputeResult<T> — non-null emits a Signal (and optionally triggers a Workflow), null is silent. Each Sense has its own SQLite database. Scheduling (interval, on) is configured in nerve.yaml.
Signal A notification emitted when a Sense returns non-null. Pure fact, no intent. Distributed via an in-memory Signal Bus. Not persisted.
Workflow A stateful multi-step execution. Contains Roles (actors with side effects) and a Moderator (pure router). Each instance is a Thread with a unique runId.
Log Immutable audit trail. Records executions, state transitions, errors. Cannot trigger Senses — prevents feedback loops.
Engine The kernel orchestrating everything. Holds Signal Bus, Process Manager, Workflow Manager. Never loads user code directly — all user code runs in isolated Workers.
Daemon The nerve-daemon package — engine runtime. Runs as a background process.

Architecture Rules

  • Two extension points: Sense (what to observe + when), Workflow (what to do)
  • Process isolation: One worker per Sense group (long-lived), one per Workflow type (on-demand). Workers never talk to each other.
  • Causality is one-directional: External world → Sense → Signal → Workflow + Log. Logs are the end of the chain.

Language & Paradigm

Functional-first

Use function + type, not class + interface.

// ✅ Good
type Signal = {
  senseId: string;
  value: unknown;
  ts: number;
};

function createSignal(senseId: string, value: unknown): Signal {
  return { senseId, value, ts: Date.now() };
}

// ❌ Bad — no class, no interface
class Signal implements ISignal { ... }

Rules

Rule Description
type over interface All type definitions use type
function over class Pure functions + closures, no class
No this Functions must not depend on this context
No inheritance No extends, implements, abstract
Composition over inheritance Use function composition
Immutability first Use Readonly<T>, as const, avoid mutation
No optional properties Use T | null instead of ?: — see below

Exceptions

Classes are allowed when:

  • Required by a third-party library (e.g. Drizzle's sqliteTable)
  • Error subclasses (class NerveError extends Error)

No Optional Properties

Never use ?:. All nullable fields must be explicit T | null.

// ✅ Good
type SenseConfig = {
  group: string;
  throttle: string | null;
  timeout: string | null;
};

// ❌ Bad
type SenseConfig = {
  group: string;
  throttle?: string;
  timeout?: string;
};

For mutually exclusive fields, use discriminated unions:

// ✅ Good
type ComputeResult<T> =
  | null
  | { signal: T; workflow: WorkflowTrigger | null };

Workflow authoring (user modules)

Roles and moderators take ThreadContext (threadId, start, steps) — not separate StartStep / message arrays.

import type { RoleResult, ThreadContext, WorkflowDefinition } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";

type MyMeta = { round: number };

async function planner(ctx: ThreadContext): Promise<RoleResult<MyMeta>> {
  void ctx.start;
  void ctx.steps;
  return { content: "plan", meta: { round: ctx.steps.length } };
}

const workflow: WorkflowDefinition<Record<"planner", MyMeta>> = {
  name: "example",
  roles: { planner },
  moderator(ctx: ThreadContext<Record<"planner", MyMeta>>) {
    return ctx.steps.length === 0 ? "planner" : END;
  },
};

Modules & Exports

  • Always named exports, never default exports
  • One module = one responsibility, filename = purpose

Naming

Type Style Example
Files kebab-case signal-bus.ts
Types PascalCase SignalBus
Functions/variables camelCase createSignalBus
Constants UPPER_SNAKE MAX_RETRY_COUNT
Generics Single letter or descriptive T, TValue

Error Handling

  • Use Result type for expected failures
  • throw only for unrecoverable bugs (programmer errors)
  • No try-catch for flow control
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

Async

  • Always async/await, never .then() chains

No Dynamic Import

Do NOT use await import() in production code. Always use static top-level import.

Exceptions (must include a comment):

  1. sense-runtime.ts — user module paths known only at runtime
  2. workflow-worker.ts — user module paths known only at runtime

Test files (__tests__/**) are exempt.

Toolchain

Tool Purpose
pnpm Package manager
TypeScript Type checking (strict mode)
Biome Lint + format (replaces ESLint + Prettier)
tsup Bundling

Commands

pnpm run check      # biome check (lint + format)
pnpm run format     # biome format --write
pnpm run build      # full build
pnpm test           # run tests

Monorepo Structure

nerve/
  packages/
    core/           # @nerve/core — shared types and utils
    cli/            # @nerve/cli — CLI entry point
    daemon/         # @nerve/daemon — engine runtime
  docs/             # RFCs, conventions
  • core is the shared layer; cli and daemon both depend on it
  • cli and daemon must NOT depend on each other

Commit Convention

<type>(<scope>): <description>

type: feat | fix | refactor | docs | chore | test
scope: core | cli | daemon | rfc-001 | ...