7.5 KiB
RFC-004: Package Architecture — Shareable Workflows, Roles & Senses
Author: 小橘 🍊(NEKO Team) Status: Draft Created: 2026-04-29
Summary
Make workflows, roles, and senses publishable as lightweight npm packages. Workspaces become pure configuration — selecting packages, wiring adapters, and providing credentials. No builtin workflows in the nerve core.
Motivation
Currently, workflows like develop-sense and develop-workflow live inside the workspace (~/.uncaged-nerve/workflows/). This creates problems:
- No sharing — every workspace duplicates the same workflow code
- No versioning — upgrading a workflow means manual file edits
- Builtin is a trap — if we bake workflows into nerve core, they require adapters and LLM providers that may not be installed. A fresh
nerveinstall on a bare machine would fail to load builtins. - Roles are already shared —
_shared/workspace-committer.tsproves the pattern works; we just need to formalize it as packages
The adapter pattern (@uncaged/nerve-adapter-hermes, @uncaged/nerve-adapter-cursor) already established the precedent: infrastructure as packages, workspace as wiring.
Design
Package Taxonomy
@uncaged/nerve-core # types, engine
@uncaged/nerve-daemon # runtime
@uncaged/nerve-workflow-utils # createRole, decorateRole, withDryRun, onFail, etc.
# Adapters (existing)
@uncaged/nerve-adapter-hermes
@uncaged/nerve-adapter-cursor
# Workflows (new)
@uncaged/nerve-workflow-solve-issue
@uncaged/nerve-workflow-develop-sense
@uncaged/nerve-workflow-develop-workflow
# Shared Roles (new)
@uncaged/nerve-role-committer # workspace committer (branch, commit, push)
@uncaged/nerve-role-reviewer # code review role
@uncaged/nerve-role-publisher # PR creation role
# Senses (existing pattern, formalized)
@uncaged/nerve-sense-cpu-usage
@uncaged/nerve-sense-disk-usage
Package Contract
Each package type exports a factory function:
Workflow Package
// @uncaged/nerve-workflow-develop-sense
import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
export type SenseMeta = { /* ... */ };
export type CreateDevelopSenseDeps = {
defaultAdapter: AgentFn;
adapters?: Partial<Record<keyof SenseMeta, AgentFn>>;
extract: LlmExtractorConfig;
cwd: string;
};
export function createDevelopSenseWorkflow(deps: CreateDevelopSenseDeps): WorkflowDefinition<SenseMeta>;
Key design decisions:
defaultAdapter+ optionaladaptersoverride per role — viaPartial<Record<keyof Meta, AgentFn>>- Adapter keys are derived from
Metatype — adding/removing a role automatically updates the adapter map - Roles that don't need an agent simply don't appear in
adapters(thePartialallows this)
Role Package
// @uncaged/nerve-role-committer
import type { AgentFn, Role } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole, decorateRole, withDryRun, onFail } from "@uncaged/nerve-workflow-utils";
export type CommitterMeta = { committed: boolean };
export function createCommitterRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<CommitterMeta> {
const inner = createRole(adapter, prompt, committerMetaSchema, extract);
return decorateRole(inner, [
withDryRun({ label: "committer", meta: { committed: true } }),
onFail({ label: "committer", meta: { committed: false } }),
]);
}
Roles compose with the decorator chain from workflow-utils:
withDryRun— skip execution in dry-run modeonFail— catch errors into structured failure resultsdecorateRole(role, [...])— apply decorators left-to-right- Custom
RoleDecorator<M>can be created for project-specific needs
Sense Package
// @uncaged/nerve-sense-cpu-usage
export const senseName = "cpu-usage";
export const schema = { /* drizzle schema */ };
export async function compute(ctx: SenseContext): Promise<SenseResult>;
Workspace as Configuration
The workspace becomes a thin wiring layer:
~/.uncaged-nerve/
nerve.yaml # senses, extract config
package.json # depends on workflow/role/adapter packages
workflows/
develop-sense/
index.ts # ~10 lines: import package, wire adapters, export
solve-issue/
index.ts # same pattern
A typical index.ts:
import { createDevelopSenseWorkflow } from "@uncaged/nerve-workflow-develop-sense";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import { cursorAdapter } from "@uncaged/nerve-adapter-cursor";
export default createDevelopSenseWorkflow({
defaultAdapter: hermesAdapter,
adapters: { planner: cursorAdapter, coder: cursorAdapter },
extract: { provider: { apiKey, baseUrl, model } },
cwd: nerveRoot,
});
What Stays in Workspace
- Custom workflows — project-specific workflows that aren't general enough to share
- Custom senses — project-specific metrics
- Configuration — adapter selection, credentials,
nerve.yaml - Overrides — a workspace can always write its own role/workflow instead of using a package
Dependency Rules
nerve-core ← no deps on other nerve packages
nerve-workflow-utils ← depends on nerve-core
nerve-adapter-* ← depends on nerve-core
nerve-role-* ← depends on nerve-core, nerve-workflow-utils
nerve-workflow-* ← depends on nerve-core, nerve-workflow-utils, may depend on nerve-role-*
nerve-sense-* ← depends on nerve-core
nerve-daemon ← depends on nerve-core, nerve-store
Workflow packages depend on role packages (not adapters). Adapters are injected at the workspace level.
Migration Path
- Phase 1: Extract role packages — Start with
@uncaged/nerve-role-committer(already_shared/workspace-committer.ts). Publish, update workspace to import from package. - Phase 2: Extract workflow packages — Move
develop-senseanddevelop-workflowto packages. Workspaceindex.tsbecomes pure wiring. - Phase 3: Sense packages — Formalize sense packaging (lower priority, senses are already self-contained directories).
- Phase 4: Community — Document the package contract so others can publish workflows/roles/senses.
Not in Scope
- No builtin workflows — nerve core ships zero workflows. All workflows are packages installed by the workspace.
- No workflow marketplace/registry — just npm packages.
pnpm add @uncaged/nerve-workflow-solve-issue. - No nerve.yaml workflow declaration — workflows are still TypeScript entry points. The daemon discovers them the same way it does today.
Open Questions
- Monorepo vs separate repos? — Should workflow/role packages live in the nerve monorepo or separate repos? Monorepo is easier for coordinated releases; separate repos allow independent versioning.
- Sense package format — Senses currently bundle with esbuild. Should sense packages ship pre-bundled or as TypeScript source?
- Version coupling — How tightly should workflow packages pin
nerve-core? Peer deps with semver range?
Prior Art
- Adapter packages (
@uncaged/nerve-adapter-*) — established the factory + injection pattern _shared/workspace-committer.ts— proved roles can be shared across workflowscreateRole/decorateRole/withDryRun/onFailinworkflow-utils— building blocks that role packages composedefaultAdapter+Partial<Record<keyof Meta, AgentFn>>pattern — adapter injection without coupling