diff --git a/docs/rfc-004-package-architecture.md b/docs/rfc-004-package-architecture.md new file mode 100644 index 0000000..72358ca --- /dev/null +++ b/docs/rfc-004-package-architecture.md @@ -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>; + extract: LlmExtractorConfig; + cwd: string; +}; + +export function createDevelopSenseWorkflow(deps: CreateDevelopSenseDeps): WorkflowDefinition; +``` + +Key design decisions: +- `defaultAdapter` + optional `adapters` override per role β€” via `Partial>` +- 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 { + 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` 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; +``` + +### 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>` pattern β€” adapter injection without coupling