docs: RFC-004 package architecture — shareable workflows, roles & senses
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
# 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:
|
||||
|
||||
1. **No sharing** — every workspace duplicates the same workflow code
|
||||
2. **No versioning** — upgrading a workflow means manual file edits
|
||||
3. **Builtin is a trap** — if we bake workflows into nerve core, they require adapters and LLM providers that may not be installed. A fresh `nerve` install on a bare machine would fail to load builtins.
|
||||
4. **Roles are already shared** — `_shared/workspace-committer.ts` proves 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
|
||||
|
||||
```ts
|
||||
// @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` + optional `adapters` override per role — via `Partial<Record<keyof Meta, AgentFn>>`
|
||||
- Adapter keys are derived from `Meta` type — adding/removing a role automatically updates the adapter map
|
||||
- Roles that don't need an agent simply don't appear in `adapters` (the `Partial` allows this)
|
||||
|
||||
#### Role Package
|
||||
|
||||
```ts
|
||||
// @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 mode
|
||||
- `onFail` — catch errors into structured failure results
|
||||
- `decorateRole(role, [...])` — apply decorators left-to-right
|
||||
- Custom `RoleDecorator<M>` can be created for project-specific needs
|
||||
|
||||
#### Sense Package
|
||||
|
||||
```ts
|
||||
// @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`:
|
||||
|
||||
```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
|
||||
|
||||
1. **Phase 1: Extract role packages** — Start with `@uncaged/nerve-role-committer` (already `_shared/workspace-committer.ts`). Publish, update workspace to import from package.
|
||||
2. **Phase 2: Extract workflow packages** — Move `develop-sense` and `develop-workflow` to packages. Workspace `index.ts` becomes pure wiring.
|
||||
3. **Phase 3: Sense packages** — Formalize sense packaging (lower priority, senses are already self-contained directories).
|
||||
4. **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
|
||||
|
||||
1. **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.
|
||||
2. **Sense package format** — Senses currently bundle with esbuild. Should sense packages ship pre-bundled or as TypeScript source?
|
||||
3. **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 workflows
|
||||
- `createRole` / `decorateRole` / `withDryRun` / `onFail` in `workflow-utils` — building blocks that role packages compose
|
||||
- `defaultAdapter` + `Partial<Record<keyof Meta, AgentFn>>` pattern — adapter injection without coupling
|
||||
Reference in New Issue
Block a user