diff --git a/.knowledge/adapter.md b/.knowledge/adapter.md index fe6baf4..a70c9ef 100644 --- a/.knowledge/adapter.md +++ b/.knowledge/adapter.md @@ -28,8 +28,29 @@ For long-running or incremental agent outputs: |---------|---------|------| | `@uncaged/nerve-adapter-cursor` | `cursorAdapter` / `createCursorAdapter()` | cursor-agent CLI | | `@uncaged/nerve-adapter-hermes` | `hermesAdapter` / `createHermesAdapter()` | hermes chat CLI | +| `@uncaged/nerve-workflow-utils` | `createLlmAdapter(provider)` | OpenAI-compatible HTTP chat (single-turn) | -Each exports a **default instance** (sensible defaults) and a **factory** for custom config. +The Cursor and Hermes adapter packages each export a **default instance** (sensible defaults) and a **factory** for custom config. `createLlmAdapter` is a factory on `@uncaged/nerve-workflow-utils` only. + +## createLlmAdapter + +`createLlmAdapter` builds an `AgentFn` from an `LlmProvider` (`baseUrl`, `apiKey`, `model`). One chat completion per role step: **system** = the string passed by `createRole` (your prompt); **user** = `ctx.start.content` (the thread’s start frame). On failure it throws with a formatted LLM error. + +```ts +import { createLlmAdapter, createRole } from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +const metaSchema = z.object({ ok: z.boolean() }); + +const planner = createRole( + createLlmAdapter({ baseUrl: "https://api.example.com/v1", apiKey: "…", model: "gpt-4o-mini" }), + "You are a planner…", + metaSchema, + extractConfig, +); +``` + +Use this when you want a role backed by an HTTP LLM instead of a subprocess CLI adapter. ## Usage in Workflows diff --git a/.knowledge/workflow.md b/.knowledge/workflow.md index 1ec8e95..8401764 100644 --- a/.knowledge/workflow.md +++ b/.knowledge/workflow.md @@ -2,6 +2,20 @@ Stateful multi-step execution driven by Roles and a Moderator. +## Workspace Layout (authoring) + +User Nerve workspaces use a **flat** build: one root `package.json`, one root bundle script (typically `scripts/build.mjs` wired from `scripts.build`), and **no** per-workflow `package.json` or `tsconfig.json`. + +| Location | Purpose | +|----------|---------| +| `workflows//index.ts` | Default export: `WorkflowDefinition` (moderator + role map). | +| `workflows//roles/.ts` | One module per role — schemas, prompts, `createRole` factories, or hand-written async role functions. | +| `dist/workflows//index.js` | Emit of the root build; this is what the daemon loads. | + +**Naming:** Workflow ids should be **verb-first** kebab-case phrases (e.g. `deploy-staging`, `scan-dependencies`), not opaque nouns alone. + +Senses follow the same flat pattern: `senses//src/*.ts`, `migrations/`, root build → `dist/senses//index.js`. See `.knowledge/sense.md`. + ## Core Concepts - **Workflow** — definition with concurrency strategy diff --git a/packages/cli/src/__tests__/e2e-validate-init.test.ts b/packages/cli/src/__tests__/e2e-validate-init.test.ts index 0abeb6f..a9fa58e 100644 --- a/packages/cli/src/__tests__/e2e-validate-init.test.ts +++ b/packages/cli/src/__tests__/e2e-validate-init.test.ts @@ -205,6 +205,10 @@ describe("e2e init", () => { expect(existsSync(join(nerveRoot, "scripts", "build.mjs"))).toBe(true); expect(existsSync(join(nerveRoot, "biome.json"))).toBe(true); expect(existsSync(join(nerveRoot, ".gitignore"))).toBe(true); + expect(existsSync(join(nerveRoot, "AGENT.md"))).toBe(true); + const agentMd = readFileSync(join(nerveRoot, "AGENT.md"), "utf8"); + expect(agentMd).toContain("verb-first"); + expect(agentMd).toContain("createRole"); expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "src", "index.ts"))).toBe(true); expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "src", "schema.ts"))).toBe(true); expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "migrations", "0001_init.sql"))).toBe( diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index b6f54ff..d3eda8e 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -127,6 +127,70 @@ node_modules/ knowledge.db `; +/** Generated at workspace root so agents can \`cat AGENT.md\` instead of npm skill paths. */ +const AGENT_MD = `# Nerve workspace — agent guide + +This file is created by \`nerve init\`. Read it before implementing senses or workflows. + +## Directory layout + +| Path | Purpose | +|------|---------| +| \`nerve.yaml\` | Senses, workflows, intervals, groups | +| \`package.json\` | Single root package — no per-sense/per-workflow packages | +| \`scripts/build.mjs\` | Root esbuild step; output under \`dist/\` | +| \`senses//src/index.ts\` | Sense \`compute()\` entry | +| \`senses//src/schema.ts\` | Drizzle SQLite schema (TypeScript) | +| \`senses//migrations/*.sql\` | SQL migrations (next to \`src/\`, not inside it) | +| \`workflows//index.ts\` | Default export: \`WorkflowDefinition\` | +| \`workflows//roles/.ts\` | One TypeScript file per role | +| \`dist/senses//index.js\` | Bundled sense (after build) | +| \`dist/workflows//index.js\` | Bundled workflow (after build) | + +There is **no** \`package.json\` or \`tsconfig.json\` inside individual senses or workflows. + +## Naming + +- **Workflows:** verb-first kebab-case (e.g. \`review-pull-request\`, \`deploy-staging\`). Avoid bare nouns like \`notifications\`. +- **Senses:** kebab-case descriptive nouns (e.g. \`cpu-usage\`). + +## Workflow roles — four-tuple pattern + +Wire each role with \`createRole\` from \`@uncaged/nerve-workflow-utils\`: + +1. **Adapter** — \`AgentFn\` (LLM call) +2. **Prompt builder** — \`async (ctx: ThreadContext) => string\` +3. **Meta schema** — Zod object (routing / structured output from the model) +4. **Extractor config** — how JSON meta is parsed from replies + +Keep meta small (often one boolean per role). The **moderator** in \`WorkflowDefinition\` routes between role names. + +## Build commands + +Always run from the **workspace root**: + +\`\`\`bash +pnpm run build +# or: npm run build +\`\`\` + +Fix errors until this succeeds. New workflows must appear under \`workflows//\` and be registered in \`nerve.yaml\`; new senses under \`senses//\` with matching \`nerve.yaml\` entries. + +## Coding style (Nerve conventions) + +- Use \`type\`, not \`interface\`; prefer \`function\` over classes (except errors / library requirements). +- **Named exports only** — no \`export default\`. +- Nullable fields: \`T | null\`, not TypeScript optional \`?:\`. +- No dynamic \`import()\` in workspace code (bundling and tooling assume static imports). +- Use \`async\`/\`await\`; use a \`Result\` type for expected failures instead of control-flow try/catch. + +## Extra references (optional) + +- \`CONVENTIONS.md\` — project-specific overrides at repo root. +- \`.knowledge/*.md\` — deeper docs when working inside the Nerve monorepo. +- \`.cursor/skills/\` — Cursor Agent Skills (\`SKILL.md\` per skill). +`; + const NERVE_SKILLS_MDC = `--- description: >- Where Agent Skills live in this Nerve workspace and how to use them with Cursor @@ -362,6 +426,7 @@ async function runInitWorkspace(force: boolean, skipInstall = false): Promise; export function coderPrompt({ threadId }: { threadId: string }): string { return `Read the workflow thread for the planner's sense design and any tester feedback: \`nerve thread ${threadId}\` -Read the nerve-dev skill for sense file structure and conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` +Read \`cat AGENT.md\` from the repository root, then \`CONVENTIONS.md\` and \`.knowledge/sense.md\` if present. ## Your task @@ -20,21 +20,21 @@ Implement (or fix) the sense the planner designed. If there is tester feedback i You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration. -## File structure for each sense +## File structure for each sense (flat workspace) -- \`senses//src/index.ts\` — TypeScript compute source; import schema as \`./schema.ts\` +The workspace has **one root** \`package.json\` and root \`scripts/build.mjs\` (or equivalent) that bundles all senses. There is **no** per-sense \`package.json\`. Bundled output is \`dist/senses//index.js\` after a root build. + +- \`senses//src/index.ts\` — compute entry; import schema as \`./schema.ts\` - \`senses//src/schema.ts\` — Drizzle schema (TypeScript) -- \`senses//migrations/\` — Drizzle migration files (at sense root, not inside src/) -- \`senses//package.json\` — with esbuild build script -- \`senses//index.js\` — bundled output generated by \`pnpm build\` (do NOT edit by hand) +- \`senses//migrations/\` — SQL migration files (at sense root, not inside \`src/\`) -Look at existing senses for the package.json template and patterns. +Look at existing senses for patterns. ## When to return done: true Return \`done: true\` ONLY when ALL of the following are true: - All required files are created -- \`pnpm install --no-cache && pnpm build\` succeeds (run it!) +- From the **workspace root**, \`pnpm run build\` or \`npm run build\` succeeds (run it!) and \`dist/senses//index.js\` exists - \`nerve.yaml\` is updated with the sense config Return \`done: false\` if you made progress but there is still work to do.`; diff --git a/packages/workflow-meta/src/develop-sense/roles/planner.ts b/packages/workflow-meta/src/develop-sense/roles/planner.ts index f9f8715..ac3672b 100644 --- a/packages/workflow-meta/src/develop-sense/roles/planner.ts +++ b/packages/workflow-meta/src/develop-sense/roles/planner.ts @@ -12,7 +12,7 @@ export function plannerPrompt({ threadId }: { threadId: string }): string { return `You are planning a new Nerve sense. Read the workflow thread for the user's request: \`nerve thread ${threadId}\` -Read the nerve-dev skill for sense conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` +Read the workspace guide: \`cat AGENT.md\` from the repository root (created by \`nerve init\`). Also read \`CONVENTIONS.md\` and \`.knowledge/sense.md\` if present. Optional skills live under \`.cursor/skills/\`. Also look at existing senses in the \`senses/\` directory for patterns. Pick a good kebab-case name for this sense. Produce a PLAN (not code) in markdown: diff --git a/packages/workflow-meta/src/develop-sense/roles/tester.ts b/packages/workflow-meta/src/develop-sense/roles/tester.ts index 4ca663a..c96ab0e 100644 --- a/packages/workflow-meta/src/develop-sense/roles/tester.ts +++ b/packages/workflow-meta/src/develop-sense/roles/tester.ts @@ -17,21 +17,20 @@ export function testerPrompt({ **IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. All paths below are relative to this directory. Always \`cd ${nerveRoot}\` first.** Read the workflow thread for context: \`nerve thread ${threadId}\` -Read the nerve-dev skill for expected file structure: \`cat ${nerveRoot}/node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` +Read \`cat ${nerveRoot}/AGENT.md\`, then \`${nerveRoot}/CONVENTIONS.md\` and \`${nerveRoot}/.knowledge/sense.md\` if they exist. Verify the full lifecycle in this order: -1. **File check** — all required sense files exist: +1. **File check** — all required sense files exist (no per-sense \`package.json\`): - \`senses//src/index.ts\` - \`senses//src/schema.ts\` - \`senses//migrations/\` - - \`senses//package.json\` -2. **Build** — run inside the sense directory: +2. **Build** — from the workspace root: \`\`\` - cd ${nerveRoot}/senses/ && pnpm install --no-cache && pnpm build + cd ${nerveRoot} && pnpm run build \`\`\` - Must produce \`index.js\` at sense root without errors. + (or \`npm run build\` per root \`package.json\`.) Must produce \`${nerveRoot}/dist/senses//index.js\` without errors. 3. **Config check** — \`nerve validate\` passes, confirming nerve.yaml is valid. diff --git a/packages/workflow-meta/src/develop-workflow/roles/coder.ts b/packages/workflow-meta/src/develop-workflow/roles/coder.ts index 6098974..ec746c2 100644 --- a/packages/workflow-meta/src/develop-workflow/roles/coder.ts +++ b/packages/workflow-meta/src/develop-workflow/roles/coder.ts @@ -10,7 +10,7 @@ export type CoderMeta = z.infer; export function coderPrompt({ threadId }: { threadId: string }): string { return `Read the workflow thread to get the planner's design and any reviewer/tester/committer feedback: \`nerve thread ${threadId}\` -Read the nerve-dev skill for workflow file structure and conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` +Read \`cat AGENT.md\` from the repository root, then \`CONVENTIONS.md\` and \`.knowledge/workflow.md\` if present. Optional skills live under \`.cursor/skills/\`. Also look at existing workflows in the \`workflows/\` directory for patterns. ## Your task @@ -29,15 +29,13 @@ You do NOT need to finish everything in one pass. You may return \`done: false\` 2. Second pass: implement role logic 3. Third pass: fix build/lint errors -## Workflow file structure +## Workflow file structure (flat workspace) + +The workspace has **one root** \`package.json\` and **one** root build (\`pnpm run build\` or \`npm run build\`), implemented by \`scripts/build.mjs\`, which emits bundles under \`dist/workflows//index.js\`. There is **no** per-workflow \`package.json\` or \`tsconfig.json\`. Each workflow must have: -- \`workflows//index.ts\` — WorkflowDefinition default export -- \`workflows//build.ts\` — factory function -- \`workflows//moderator.ts\` — moderator + meta types -- \`workflows//roles/.ts\` — meta schema and prompt function per role -- \`workflows//package.json\` — with esbuild build script -- \`workflows//tsconfig.json\` — TypeScript config +- \`workflows//index.ts\` — default export \`WorkflowDefinition\` (moderator and meta types typically live here or are imported from co-located modules) +- \`workflows//roles/.ts\` — one TypeScript file per role (schemas, prompts, \`createRole\` wiring, or plain async role functions) For **new workflows**, also update \`nerve.yaml\` with \`workflows.\`. @@ -53,7 +51,7 @@ For **new workflows**, also update \`nerve.yaml\` with \`workflows.\`. Return \`done: true\` ONLY when ALL of the following are true: - All changes from the plan are implemented -- \`cd workflows/ && pnpm install --no-cache && pnpm build\` succeeds (run it!) +- From the **workspace root**, \`pnpm run build\` or \`npm run build\` succeeds (run it!) so \`dist/workflows//index.js\` is produced - No lint or type errors remain Return \`done: false\` if you made progress but there is still work to do, or if build/lint has errors you plan to fix in the next iteration.`; diff --git a/packages/workflow-meta/src/develop-workflow/roles/planner.ts b/packages/workflow-meta/src/develop-workflow/roles/planner.ts index 85f22aa..eadc893 100644 --- a/packages/workflow-meta/src/develop-workflow/roles/planner.ts +++ b/packages/workflow-meta/src/develop-workflow/roles/planner.ts @@ -12,18 +12,18 @@ export function plannerPrompt({ threadId }: { threadId: string }): string { return `You are a Nerve workflow planner. You can **create new workflows** or **modify existing ones**. Read the workflow thread for the user's request: \`nerve thread ${threadId}\` -Read the nerve-dev skill for workflow conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` +Read the workspace guide: \`cat AGENT.md\` from the repository root (created by \`nerve init\`). Also read \`CONVENTIONS.md\` if it exists; if \`.knowledge/workflow.md\` exists (e.g. Nerve monorepo), read it for layout and engine behavior. Optional Cursor skills live under \`.cursor/skills/\`. List existing workflows: \`ls workflows/\` ## Determine the task type -1. If the user wants to **modify an existing workflow** — read its current code (\`cat workflows//moderator.ts\`, \`cat workflows//build.ts\`, \`ls workflows//roles/\`, etc.) and understand its current structure before planning changes. +1. If the user wants to **modify an existing workflow** — read its current code (\`cat workflows//index.ts\`, \`ls workflows//roles/\`, \`cat workflows//roles/.ts\`, etc.) and understand its current structure before planning changes. 2. If the user wants to **create a new workflow** — look at existing workflows in \`workflows/\` for patterns to follow. ## Produce a PLAN (not code) in markdown For **new workflows**: -- Workflow name (kebab-case) +- Workflow name — **verb-first** kebab-case phrase (e.g. \`review-pull-request\`, \`deploy-staging\`), not a bare noun - Roles list (name, purpose, tool) - Flow transitions / moderator routing logic - Validation loops design diff --git a/packages/workflow-meta/src/develop-workflow/roles/tester.ts b/packages/workflow-meta/src/develop-workflow/roles/tester.ts index 2a35ef6..8902732 100644 --- a/packages/workflow-meta/src/develop-workflow/roles/tester.ts +++ b/packages/workflow-meta/src/develop-workflow/roles/tester.ts @@ -17,24 +17,22 @@ export function testerPrompt({ **IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. All paths below are relative to this directory. Always \`cd ${nerveRoot}\` first.** Read the workflow thread for context: \`nerve thread ${threadId}\` -Read the nerve-dev skill for expected file structure: \`cat ${nerveRoot}/node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` +Read \`cat ${nerveRoot}/AGENT.md\`, then \`${nerveRoot}/CONVENTIONS.md\` and \`${nerveRoot}/.knowledge/workflow.md\` if they exist. Get the workflow name from the thread (the planner's output). Verify the full lifecycle in this order: -1. **File check** — all required workflow files exist (under \`${nerveRoot}/\`): +1. **File check** — all required workflow sources exist (under \`${nerveRoot}/\`): - \`workflows//index.ts\` - - \`workflows//build.ts\` - - \`workflows//moderator.ts\` - - \`workflows//roles/\` with one \`.ts\` file per role - - \`workflows//package.json\` + - \`workflows//roles/\` with one \`.ts\` file per role (flat files, not per-role packages) + - **No** \`workflows//package.json\` or \`tsconfig.json\` expected -2. **Build** — run inside the workflow directory: +2. **Build** — from the workspace root: \`\`\` - cd ${nerveRoot}/workflows/ && pnpm install --no-cache && pnpm build + cd ${nerveRoot} && pnpm run build \`\`\` - Must produce \`dist/index.js\` without errors. + (or \`npm run build\` if that is what the root \`package.json\` defines.) Must produce \`${nerveRoot}/dist/workflows//index.js\` without errors. 3. **Config check** — \`cd ${nerveRoot} && nerve validate\` passes, confirming nerve.yaml is valid.