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
T
xiaoju fc7fc9158c docs: update all docs/conventions for stateful sense, remove stale refs
Phase 4 of RFC #308: Stateful Sense refactor.

- CLAUDE.md: updated diagram, tables, examples (no more Signal)
- Cleaned stale Signal Bus / DrizzleDB / _signals / retention refs
  across READMEs, .cursor rules, copilot instructions, .knowledge
- Removed drizzle-orm from core package.json (no longer used)
- Updated pnpm-lock.yaml

Refs #308
2026-05-01 10:09:01 +00:00

6.3 KiB

Nerve Coding Conventions

Core Concepts

External World → Sense(state) → { newState, workflow? } → 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 stateful compute(state) function. Returns new state and an optional workflow trigger. State persisted as JSON. Scheduling configured in nerve.yaml.
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 Process Manager, Workflow Manager, Sense Scheduler. 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(state) → Workflow (when triggered) + Log. Logs are the end of the chain.

Language & Paradigm

Functional-first

Use function + type, not class + interface.

// ✅ Good
type WorkflowLaunch = {
  senseName: string;
  workflowName: string;
  ts: number;
};

function recordWorkflowLaunch(senseName: string, workflowName: string): WorkflowLaunch {
  return { senseName, workflowName, ts: Date.now() };
}

// ❌ Bad — no class, no interface
class WorkflowLaunch implements IWorkflowLaunch { ... }

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 — sense modules return explicit next state + optional workflow trigger
type SenseComputeReturn<S> = {
  state: S;
  workflow: WorkflowTrigger | null;
};

Workflow Naming

Workflow identifiers — WorkflowDefinition.name, the directory under workflows/, and keys under workflows: in nerve.yaml — must use verb-first kebab-case phrases so the name reads as an action.

  • solve-issue, extract-knowledge, develop-sense
  • knowledge-extraction, issue-solver

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 sense-scheduler.ts
Types PascalCase SenseScheduler
Functions/variables camelCase createSenseScheduler
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 | ...