# 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`. ```typescript // ✅ 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`, `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`. ```typescript // ✅ 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: ```typescript // ✅ Good — sense modules return explicit next state + optional workflow trigger type SenseComputeReturn = { 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. ```typescript 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> { void ctx.start; void ctx.steps; return { content: "plan", meta: { round: ctx.steps.length } }; } const workflow: WorkflowDefinition> = { name: "example", roles: { planner }, moderator(ctx: ThreadContext>) { 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 ```typescript type Result = { 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 ```bash 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: feat | fix | refactor | docs | chore | test scope: core | cli | daemon | rfc-001 | ... ```