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/packages/skills/nerve-dev/SKILL.md
T
xiaoju 8dd82d99da refactor(core): remove WorkflowTrigger from SenseTrigger — shell only
Senses trigger shell commands only. Workflows are invoked via CLI.

SenseTrigger is now { command: string } — no discriminated union.

Closes #318

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-02 12:33:38 +00:00

17 KiB

name, description, license, compatibility, metadata
name description license compatibility metadata
nerve-dev Nerve development guide for AI coding agents. Covers architecture (sense → signal → workflow), CLI commands, sense and workflow development patterns, nerve.yaml configuration, coding conventions, and common pitfalls. Use when developing senses, workflows, or contributing to the Nerve codebase. MIT Requires Node.js 22+, pnpm, TypeScript. Nerve monorepo with packages/core, packages/cli, packages/daemon.
author version
uncaged 1.0

Nerve 开发指南

Nerve 是一个轻量级本地感知引擎,为自主 agent 持续观察外部状态、响应变化、编排多步骤工作流。

核心数据流

External World → Sense → Signal → Workflow → Log
                  ↑                              ↑
           "观察什么"                       "做什么"
概念 说明
Sense compute() 函数,采样或派生数据。返回 T | null — 非 null 发出 Signal,null 静默。每个 Sense 有自己的 SQLite 数据库
Signal Sense 返回非 null 时发出的通知。纯事实,无意图。通过内存 Signal Bus 分发,不持久化
Workflow 有状态的多步骤执行。包含 Roles(有副作用的执行者)和 Moderator(纯路由器)。每个实例是一个 Thread
Log 不可变审计日志。记录执行、状态变更、错误
Kernel 编排核心。持有 Signal Bus、调度器、进程管理、Workflow Manager
Daemon nerve-daemon — 后台运行的引擎进程

用户工作区 ~/.uncaged-nerve/

~/.uncaged-nerve/
  nerve.yaml        # 主配置
  package.json      # 依赖(@uncaged/nerve-core, drizzle-orm 等)
  senses/           # sense 模块
    cpu-usage/
      package.json  # esbuild 构建脚本
      src/
        index.ts    # compute 函数(TypeScript 源码)
        schema.ts   # Drizzle ORM schema
      index.js      # 构建产物(由 pnpm run build 生成)
      migrations/   # SQL 迁移
  workflows/        # workflow 模块
    alert/
      index.ts      # WorkflowDefinition
  data/             # SQLite 数据库、signal 存储
  logs/             # 日志

nerve.yaml 配置

senses:
  cpu-usage:
    group: system         # worker 分组(同组 sense 共享一个 worker 进程)
    throttle: 5s          # 最小计算间隔
    timeout: 10s          # compute 超时
    grace_period: null    # 首次错误后的宽限期
    retention: 10000      # _signals 表最大行数
    interval: 10s         # 定时轮询周期
    on: [disk-usage]      # 响应其他 sense 的 signal 触发计算

  disk-usage:
    group: system
    throttle: 30s

workflows:
  alert:
    concurrency: 1        # 最大并发 thread 数
    overflow: queue        # 超出时排队(或 drop)

max_rounds: 10            # workflow 默认最大轮数

api:                      # HTTP API 配置
  bind: "127.0.0.1:9800"
  token: null             # loopback 不强制 token

调度方式

  • interval: 10s — 每 10 秒触发一次 compute
  • on: [other-sense] — 当 other-sense 发出 signal 时触发
  • 两者可组合

Sense 开发

Sense Anatomy

一个 sense 由以下文件组成(nerve create sense <name> 自动生成):

senses/<name>/
  package.json      # esbuild 构建脚本 + devDependencies
  src/
    index.ts        # compute() 函数(TypeScript 源码)— 必须
    schema.ts       # Drizzle ORM 表定义 — 推荐
  index.js          # 构建产物(daemon 实际加载此文件)— 由 pnpm run build 生成
  migrations/       # SQL 迁移文件
    0001_init.sql   # 建表 SQL — Drizzle Kit 生成

命名规范:sense ID 用 kebab-case(如 cpu-usage),对应 SQL 表名自动转 snake_case(cpu_usage)。

构建流程nerve create sense <name> 脚手架后会自动运行 pnpm install && pnpm run build,将 src/index.ts 打包为 index.js。修改源码后需手动重新构建:

cd ~/.uncaged-nerve/senses/<name>
pnpm run build

compute 函数

compute 返回 null(静默)或 { signal, workflow } 结构:

// senses/cpu-usage/src/index.ts
import type { LibSQLDatabase } from "drizzle-orm/libsql";
import { cpuUsage } from "./schema.js";

type SenseResult = {
  signal: { cpu: number; ts: number };
  workflow: null;
} | null;

export async function compute(
  db: LibSQLDatabase,
  _peers: Record<string, LibSQLDatabase>,
  _options: { signal: AbortSignal },
): Promise<SenseResult> {
  void cpuUsage;
  // db: Drizzle ORM SQLite 实例(读写,当前 sense 专用)
  // _peers: Record<string, DrizzleDB>(只读,同组其他 sense 的 DB)
  // _options: { signal: AbortSignal }

  const usage = getCpuUsage();

  // 返回 null → 静默(不发 Signal)
  if (usage <= 80) return null;

  // 返回 { signal, workflow } → 发 Signal,可选触发 Workflow
  return {
    signal: { cpu: usage, ts: Date.now() },
    workflow: null, // 不触发 workflow
  };
}

Shell trigger vs Workflow:Sense compute 只能请求 { command: string },由 worker 执行 shell。要跑 workflow,请在命令里调用 CLI(例如 nerve workflow trigger <name> ...)或由外部通过 daemon IPC 触发。

// 示例:异常时在 shell 里触发 workflow(需 PATH 中能调用 nerve)
export async function compute(state) {
  const anomaly = detectAnomaly();
  if (!anomaly) return { state, trigger: null };

  return {
    state: { ...state, lastAlert: Date.now() },
    trigger: {
      command:
        'nerve workflow trigger alert --prompt "CPU 持续高负载" --max-rounds 5',
    },
  };
}

SenseTrigger 类型@uncaged/nerve-core):{ command: string }。由 parseSenseTrigger 校验(仅允许 command 键)。

trigger 行为
null 只持久化 state
{ command } 持久化 state + worker 执行 shell 命令

Drizzle Schema 与迁移

Schema 使用 Drizzle ORM 定义,迁移 SQL 由 drizzle-kit generate 生成:

// senses/cpu-usage/schema.ts
import { sqliteTable, integer, real } from "drizzle-orm/sqlite-core";

export const cpuUsage = sqliteTable("cpu_usage", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  usage: real("usage").notNull(),
  ts: integer("ts").notNull(),
});

生成迁移:

npx drizzle-kit generate    # 生成 migrations/0001_*.sql

迁移文件放在 senses/<name>/migrations/,daemon 启动时自动执行。

进程隔离

  • 同一 group 的 sense 共享一个 long-lived worker 进程
  • Worker 通过 Node.js IPC(fork + process.send)与 kernel 通信
  • 每个 sense 有独立的 SQLite 数据库

nerve sense 命令

nerve sense list               # 列出所有 sense 及状态(group、throttle、trigger schedule、last signal)
nerve sense trigger <name>     # 手动触发一次 compute
nerve sense query <name>       # 查询 sense 的 SQLite 数据(默认查 _signals 表)
nerve sense query <name> "SELECT * FROM cpu_usage ORDER BY ts DESC LIMIT 10"
nerve sense schema <name>      # 查看 sense 数据库的 CREATE TABLE 语句
nerve sense schema <name> --json  # JSON 格式输出

Workflow 开发

Workflow Anatomy

一个 workflow 由以下文件组成(nerve create workflow <name> 自动生成):

workflows/<name>/
  index.ts              # WorkflowDefinition — import roles,定义 moderator
  roles/
    planner/
      index.ts          # role 构建函数
      prompt.ts         # prompt 模板(纯函数,静态 import)
    coder/
      index.ts
      prompt.ts
    tester/
      index.ts          # 手写 role(纯 CLI 逻辑,不用 factory)
    shared.ts            # 共享 helpers(provider、工具函数)
    types.ts             # SenseMeta 类型 + Zod schemas

每个 role 是一个独立目录,包含 index.ts(role 构建函数)和 prompt.ts(prompt 模板)。

设计原则:Meta 只服务 Moderator 路由

Meta 不是数据总线。 Role 之间不通过 meta 传递数据。

  • content 写入 thread,下游 role 通过 nerve thread <threadId> 自己读上下文
  • meta 只包含 moderator 做路由决策需要的最小信息
// ✅ Good — meta 只给 moderator 用
type SenseMeta = {
  planner: { senseName: string };         // moderator 不需要 plan 全文
  coder: { filesCreated: boolean };        // moderator 只需知道成功没
  tester: { passed: boolean; attempt: number };
};

// ❌ Bad — meta 当数据总线,role 间紧耦合
type SenseMeta = {
  planner: { plan: string; senseName: string; userInput: string };
  coder: { senseName: string; files: Record<string, boolean>; cursorOutput: string };
};

Role Factory 模板(@uncaged/nerve-workflow-utils

优先使用 Role factory 创建 agent 类 role,不手写 cursorAgent() + llmExtract() 调用链。

// roles/planner/index.ts
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
import { resolveDashScopeProvider, NERVE_ROOT } from "../shared.js";
import { plannerMetaSchema } from "../types.js";
import type { SenseMeta } from "../types.js";
import { plannerPrompt } from "./prompt.js";

export async function buildPlannerRole() {
  const provider = await resolveDashScopeProvider();
  if (provider === null) {
    throw new Error("Missing DASHSCOPE_API_KEY or DASHSCOPE_BASE_URL");
  }
  return createCursorRole<SenseMeta["planner"]>({
    cwd: NERVE_ROOT,
    mode: "ask",
    prompt: async (threadId) => plannerPrompt({ threadId, ... }),
    extract: { provider, schema: plannerMetaSchema },
  });
}

可用 factory:

Factory 用途
createCursorRole(opts) 调 cursor-agent CLI,适合代码生成/review
createHermesRole(opts) 调 Hermes Agent CLI,适合通用任务
createLlmRole(opts) 调 OpenAI 兼容 API,适合纯文本生成
createReActRole(opts) ReAct 循环(思考→工具→观察),适合需要工具调用的任务

所有 factory 内部流程统一:agent 执行 → content → llmExtract(content, schema) → RoleResult<Meta>

不适合用 factory 的场景:纯 CLI 逻辑(如跑测试、检查文件、git 操作)—— 这类 role 手写更清晰。

Prompt 模板

Prompt 放在独立的 prompt.ts 文件,导出纯函数:

// roles/planner/prompt.ts
export function plannerPrompt(vars: {
  threadId: string;
  senseExamples: string;
  nerveYaml: string;
}): string {
  return `You are planning a new Nerve sense.

Read the workflow thread: \`nerve thread ${vars.threadId}\`
...`;
}
// roles/planner/index.ts — 使用
import { plannerPrompt } from "./prompt.js";

prompt: async (threadId) => plannerPrompt({ threadId, senseExamples, nerveYaml }),

不要用 readFileSync 读 prompt.md — 静态 import 确保 bundler 能打包,类型安全,无运行时文件依赖。

WorkflowDefinition

// workflows/sense-generator/index.ts
import type { WorkflowDefinition } from "@uncaged/nerve-core";
import { END } from "@uncaged/nerve-core";
import { buildPlannerRole } from "./roles/planner/index.js";
import { buildCoderRole } from "./roles/coder/index.js";
import { tester } from "./roles/tester/index.js";
import type { SenseMeta } from "./roles/types.js";

const workflow: WorkflowDefinition<SenseMeta> = {
  name: "sense-generator",
  roles: {
    planner: await buildPlannerRole(),
    coder: await buildCoderRole(),
    tester,
  },
  moderator(context) {
    if (context.steps.length === 0) return "planner";
    const last = context.steps[context.steps.length - 1];
    if (last.role === "planner") return "coder";
    if (last.role === "coder") return "tester";
    if (last.role === "tester") {
      if (last.meta.passed) return END;
      return last.meta.attempt < 3 ? "coder" : END;
    }
    return END;
  },
};

export default workflow;

Top-level await 是合法的 — daemon 通过 await import(indexPath) 加载 workflow 模块。

Meta Schema 定义

Meta 类型和 Zod schema 集中在 roles/types.ts

// roles/types.ts
import { z } from "zod";

export type SenseMeta = {
  planner: { senseName: string };
  coder: { filesCreated: boolean };
  tester: { passed: boolean; attempt: number };
};

export const plannerMetaSchema = z.object({
  senseName: z.string().describe("kebab-case sense name from the plan"),
});

export const coderMetaSchema = z.object({
  filesCreated: z.boolean().describe("true if the sense files were created"),
});

公共工具(@uncaged/nerve-workflow-utils

工具 用途
createCursorRole(opts) Role factory:cursor-agent + llmExtract,返回 Role<T>
createHermesRole(opts) Role factory:Hermes Agent + llmExtract,返回 Role<T>
createLlmRole(opts) Role factory:OpenAI chat + llmExtract,返回 Role<T>
createReActRole(opts) Role factory:ReAct 循环 + llmExtract,返回 Role<T>
spawnSafe(cmd, args, opts) 安全启动子进程,shell: false,支持 timeout,返回 Result<SpawnResult>
nerveCommandEnv() 返回配置好 pnpm/nerve PATH 的环境变量
isDryRun(startStep) 从 workflow StartStep 提取 dry-run 标志

Workflow 进程模型

  • 每个 workflow 类型有一个 on-demand worker 进程(可复用)
  • 每个运行实例是一个 Thread,有唯一 runId
  • 支持崩溃恢复和 resume

nerve workflow & nerve thread 命令

# Workflow 管理
nerve workflow list            # 列出 workflow 定义(从 nerve.yaml 读取)
nerve workflow status          # 查看运行中的 workflow(从 daemon 实时获取)
nerve workflow trigger <name>  # 触发 workflow
nerve workflow trigger <name> --max-rounds 5 --prompt '分析 CPU 异常' --dry-run

# Thread(运行实例)管理
nerve thread list              # 列出运行记录(默认只显示 running/queued)
nerve thread list --all        # 包括已完成的
nerve thread show <runId>      # 查看 role 轮次对话内容
nerve thread inspect <runId>   # 查看底层事件详情
nerve thread kill <runId>      # 终止运行中的 thread

其他 CLI 命令

初始化与开发

nerve init                     # 初始化工作区
nerve init --from <git-url>    # 从 git 克隆
nerve create sense <name>      # 脚手架 sense 模板
nerve create workflow <name>   # 脚手架 workflow 模板
nerve dev                      # 前台开发模式(热重载)
nerve validate                 # 校验 nerve.yaml

Daemon 管理

nerve daemon start             # 后台启动
nerve daemon stop              # 停止
nerve daemon status            # 状态(pid、uptime、senses、workers)
nerve daemon restart            # 重启
nerve status                   # 同 daemon status
nerve stop                     # 同 daemon stop
nerve logs                     # 查看日志
nerve logs -f                  # 实时跟踪日志

Remote(多节点管理)

nerve remote add <name> <host> --token <secret>
nerve remote list
nerve remote default <name>
nerve --host remote-host:9800 sense list

存储维护

nerve store archive            # 归档 30 天前的日志
nerve store archive --vacuum   # 归档并压缩 SQLite

编码规范

语言与范式

  • Functional-first:用 type + function,不用 class + interface
  • No thisNo inheritance
  • No optional properties:用 T | null 替代 ?:
  • Result<T, E> 类型 处理预期错误,throw 仅用于不可恢复的 bug
  • Always async/await,不用 .then()
  • No dynamic import(除 sense-runtime.ts 和 workflow-worker.ts)

命名

类型 风格 示例
文件 kebab-case signal-bus.ts
类型 PascalCase SignalBus
函数/变量 camelCase createSignalBus
常量 UPPER_SNAKE MAX_RETRY_COUNT

工具链

pnpm run check      # biome check(lint + format)
pnpm run format     # biome format --write
pnpm run build      # 全量构建
pnpm test           # 运行测试

Commit 规范

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

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

开发流程

  1. 修改代码 → 在对应 package 目录下开发
  2. pnpm run build → 确保编译通过
  3. pnpm test → 确保测试通过
  4. pnpm run check → 确保 biome lint/format 通过
  5. nerve dev → 在工作区前台运行验证
  6. nerve sense list / nerve sense query <name> → 检查 sense 输出
  7. nerve logs -f → 实时查看日志排查问题

Pitfalls

  • Sense compute() 返回 undefined 和返回 null 不同 — undefined 会被当作非 null 值尝试持久化
  • 同 group 的 sense 共享 worker 进程,一个 sense 的阻塞会影响同组其他 sense
  • Workflow 的 moderator 必须是纯函数(无副作用),所有副作用放在 roles
  • Log 不能触发后续动作 — 这是防止反馈循环的设计
  • nerve.yaml 中所有 duration 字段支持 5s10m 等人类可读格式
  • Workflow directive 格式是 { workflow: { name, maxRounds, prompt, dryRun } },signal 和 workflow 是蕴含关系(先 signal 后 workflow)