refactor: migrate develop-sense/develop-workflow to @uncaged/nerve-workflow-meta

Delete local roles, moderator, and build files. Workflow index.ts
now imports factory from package and wires adapters/extract/cwd.

Closes #21

— 小橘 🍊(NEKO Team)
This commit is contained in:
小橘 2026-04-29 14:52:25 +00:00
parent b0cff7e0ed
commit 60979aaa6a
20 changed files with 17 additions and 566 deletions

View File

@ -13,6 +13,7 @@
"@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon", "@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon",
"@uncaged/nerve-role-committer": "link:../repos/nerve/packages/role-committer", "@uncaged/nerve-role-committer": "link:../repos/nerve/packages/role-committer",
"@uncaged/nerve-role-reviewer": "link:../repos/nerve/packages/role-reviewer", "@uncaged/nerve-role-reviewer": "link:../repos/nerve/packages/role-reviewer",
"@uncaged/nerve-workflow-meta": "link:../repos/nerve/packages/workflow-meta",
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils", "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
"drizzle-orm": "latest", "drizzle-orm": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"
@ -30,7 +31,8 @@
"@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon", "@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon",
"@uncaged/nerve-core": "link:../repos/nerve/packages/core", "@uncaged/nerve-core": "link:../repos/nerve/packages/core",
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils", "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
"@uncaged/nerve-role-committer": "link:../repos/nerve/packages/role-committer" "@uncaged/nerve-role-committer": "link:../repos/nerve/packages/role-committer",
"@uncaged/nerve-workflow-meta": "link:../repos/nerve/packages/workflow-meta"
} }
} }
} }

10
pnpm-lock.yaml generated
View File

@ -11,6 +11,7 @@ overrides:
'@uncaged/nerve-core': link:../repos/nerve/packages/core '@uncaged/nerve-core': link:../repos/nerve/packages/core
'@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils '@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils
'@uncaged/nerve-role-committer': link:../repos/nerve/packages/role-committer '@uncaged/nerve-role-committer': link:../repos/nerve/packages/role-committer
'@uncaged/nerve-workflow-meta': link:../repos/nerve/packages/workflow-meta
importers: importers:
@ -34,6 +35,9 @@ importers:
'@uncaged/nerve-role-reviewer': '@uncaged/nerve-role-reviewer':
specifier: link:../repos/nerve/packages/role-reviewer specifier: link:../repos/nerve/packages/role-reviewer
version: link:../repos/nerve/packages/role-reviewer version: link:../repos/nerve/packages/role-reviewer
'@uncaged/nerve-workflow-meta':
specifier: link:../repos/nerve/packages/workflow-meta
version: link:../repos/nerve/packages/workflow-meta
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../repos/nerve/packages/workflow-utils specifier: link:../repos/nerve/packages/workflow-utils
version: link:../repos/nerve/packages/workflow-utils version: link:../repos/nerve/packages/workflow-utils
@ -119,6 +123,9 @@ importers:
'@uncaged/nerve-core': '@uncaged/nerve-core':
specifier: link:../../../repos/nerve/packages/core specifier: link:../../../repos/nerve/packages/core
version: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core
'@uncaged/nerve-workflow-meta':
specifier: link:../../../repos/nerve/packages/workflow-meta
version: link:../../../repos/nerve/packages/workflow-meta
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../../../repos/nerve/packages/workflow-utils specifier: link:../../../repos/nerve/packages/workflow-utils
version: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils
@ -147,6 +154,9 @@ importers:
'@uncaged/nerve-core': '@uncaged/nerve-core':
specifier: link:../../../repos/nerve/packages/core specifier: link:../../../repos/nerve/packages/core
version: link:../../../repos/nerve/packages/core version: link:../../../repos/nerve/packages/core
'@uncaged/nerve-workflow-meta':
specifier: link:../../../repos/nerve/packages/workflow-meta
version: link:../../../repos/nerve/packages/workflow-meta
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../../../repos/nerve/packages/workflow-utils specifier: link:../../../repos/nerve/packages/workflow-utils
version: link:../../../repos/nerve/packages/workflow-utils version: link:../../../repos/nerve/packages/workflow-utils

View File

@ -1,39 +0,0 @@
import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { moderator } from "./moderator.js";
import type { SenseMeta } from "./moderator.js";
import { createCoderRole } from "./roles/coder.js";
import { createWorkspaceCommitterRole } from "./roles/committer.js";
import { createPlannerRole } from "./roles/planner.js";
import { createReviewerRole } from "./roles/reviewer.js";
import { createTesterRole } from "./roles/tester.js";
export type CreateDevelopSenseDeps = {
defaultAdapter: AgentFn;
adapters?: Partial<Record<keyof SenseMeta, AgentFn>>;
extract: LlmExtractorConfig;
cwd: string;
};
export function createDevelopSenseWorkflow({
defaultAdapter,
adapters,
extract,
cwd,
}: CreateDevelopSenseDeps): WorkflowDefinition<SenseMeta> {
const a = (role: keyof SenseMeta) => adapters?.[role] ?? defaultAdapter;
const roles = {
planner: createPlannerRole(a('planner'), extract),
coder: createCoderRole(a('coder'), extract),
reviewer: createReviewerRole(a('reviewer'), extract, { cwd, conventionsPath: "CONVENTIONS.md" }),
tester: createTesterRole(a('tester'), extract, cwd),
committer: createWorkspaceCommitterRole(a('committer'), extract),
};
return {
name: "develop-sense",
roles,
moderator,
};
}

View File

@ -1,7 +1,7 @@
import { join } from "node:path"; import { join } from "node:path";
import { createCursorAdapter, cursorAdapter } from "@uncaged/nerve-adapter-cursor"; import { createCursorAdapter, cursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import { createDevelopSenseWorkflow } from "./build.js"; import { createDevelopSenseWorkflow } from "@uncaged/nerve-workflow-meta";
const HOME = process.env.HOME ?? "/home/azureuser"; const HOME = process.env.HOME ?? "/home/azureuser";
const NERVE_ROOT = join(HOME, ".uncaged-nerve"); const NERVE_ROOT = join(HOME, ".uncaged-nerve");

View File

@ -1,65 +0,0 @@
import { END } from "@uncaged/nerve-core";
import type { Moderator } from "@uncaged/nerve-core";
import type { PlannerMeta } from "./roles/planner.js";
import type { CoderMeta } from "./roles/coder.js";
import type { ReviewerMeta } from "./roles/reviewer.js";
import type { TesterMeta } from "./roles/tester.js";
import type { CommitterMeta } from "./roles/committer.js";
export type SenseMeta = {
planner: PlannerMeta;
coder: CoderMeta;
reviewer: ReviewerMeta;
tester: TesterMeta;
committer: CommitterMeta;
};
const MAX_CODER_ROUNDS = 20;
const MAX_TOTAL_REJECTIONS = 10;
function coderRounds(steps: { role: string }[]): number {
return steps.filter((s) => s.role === "coder").length;
}
function totalRejections(steps: { role: string; meta: unknown }[]): number {
return steps.filter((s) => {
if (s.role === "reviewer") return !(s.meta as Record<string, boolean>).approved;
if (s.role === "tester") return !(s.meta as Record<string, boolean>).passed;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
return false;
}).length;
}
function canRetryCoder(steps: { role: string; meta: unknown }[]): boolean {
return coderRounds(steps) < MAX_CODER_ROUNDS && totalRejections(steps) < MAX_TOTAL_REJECTIONS;
}
export const moderator: Moderator<SenseMeta> = (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") {
if (last.meta.filesCreated) return "reviewer";
return canRetryCoder(context.steps) ? "coder" : END;
}
if (last.role === "reviewer") {
if (last.meta.approved) return "tester";
return canRetryCoder(context.steps) ? "coder" : END;
}
if (last.role === "tester") {
if (last.meta.passed) return "committer";
return canRetryCoder(context.steps) ? "coder" : END;
}
if (last.role === "committer") {
if (last.meta.committed) return END;
return canRetryCoder(context.steps) ? "coder" : END;
}
return END;
};

View File

@ -10,6 +10,7 @@
"@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-cursor": "latest",
"@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-adapter-hermes": "latest",
"@uncaged/nerve-core": "latest", "@uncaged/nerve-core": "latest",
"@uncaged/nerve-workflow-meta": "link:../../../repos/nerve/packages/workflow-meta",
"@uncaged/nerve-workflow-utils": "latest", "@uncaged/nerve-workflow-utils": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },

View File

@ -1,50 +0,0 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
export const coderMetaSchema = z.object({
filesCreated: z.boolean().describe("true if the sense files were created"),
});
export type CoderMeta = z.infer<typeof coderMetaSchema>;
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\`
## Your task
Implement (or fix) the sense the planner designed. If there is tester feedback in the thread, fix the issues it identified.
## Multi-step approach
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
- \`senses/<name>/src/index.ts\` — TypeScript compute source; import schema as \`./schema.ts\`
- \`senses/<name>/src/schema.ts\` — Drizzle schema (TypeScript)
- \`senses/<name>/migrations/\` — Drizzle migration files (at sense root, not inside src/)
- \`senses/<name>/package.json\` — with esbuild build script
- \`senses/<name>/index.js\` — bundled output generated by \`pnpm build\` (do NOT edit by hand)
Look at existing senses for the package.json template and 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!)
- \`nerve.yaml\` is updated with the sense config
Return \`done: false\` if you made progress but there is still work to do.`;
}
export function createCoderRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<CoderMeta> {
return createRole(
adapter,
async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
coderMetaSchema,
extract,
);
}

View File

@ -1,5 +0,0 @@
export {
createCommitterRole as createWorkspaceCommitterRole,
committerMetaSchema,
type CommitterMeta,
} from "@uncaged/nerve-role-committer";

View File

@ -1,36 +0,0 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
export const plannerMetaSchema = z.object({
senseName: z.string().describe("kebab-case sense name from the plan"),
});
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
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\`
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:
## Sense Design
### Name kebab-case
### Fields name, type (integer/real/text), description
### Compute Logic step-by-step, specific Node.js APIs or shell commands
### Trigger Config group, interval, throttle, timeout
Output ONLY the plan. Be precise and implementation-ready.`;
}
export function createPlannerRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<PlannerMeta> {
return createRole(
adapter,
async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
plannerMetaSchema,
extract,
);
}

View File

@ -1,3 +0,0 @@
import { createReviewerRole } from "@uncaged/nerve-role-reviewer";
export { createReviewerRole };
export type { ReviewerMeta } from "@uncaged/nerve-role-reviewer";

View File

@ -1,58 +0,0 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
export const testerMetaSchema = z.object({
passed: z.boolean().describe("true if all e2e checks passed"),
});
export type TesterMeta = z.infer<typeof testerMetaSchema>;
export function testerPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
return `You are testing a newly created Nerve sense end-to-end.
**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\`
Verify the full lifecycle in this order:
1. **File check** all required sense files exist:
- \`senses/<name>/src/index.ts\`
- \`senses/<name>/src/schema.ts\`
- \`senses/<name>/migrations/\`
- \`senses/<name>/package.json\`
2. **Build** run inside the sense directory:
\`\`\`
cd ${nerveRoot}/senses/<name> && pnpm install --no-cache && pnpm build
\`\`\`
Must produce \`index.js\` at sense root without errors.
3. **Config check** \`nerve validate\` passes, confirming nerve.yaml is valid.
4. **Sense list** \`nerve sense list\` shows the sense.
5. **Trigger** \`nerve sense trigger <name>\` completes without error.
6. **Query** \`nerve sense query <name>\` — retry up to 20s until rows appear.
If any step fails, include the relevant error output.
Output a clear summary: what you checked, what passed, what failed, and why.`;
}
export function createTesterRole(
adapter: AgentFn,
extract: LlmExtractorConfig,
nerveRoot: string,
): Role<TesterMeta> {
return createRole(
adapter,
async (start: StartStep) =>
testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
testerMetaSchema,
extract,
);
}

View File

@ -1,39 +0,0 @@
import type { AgentFn, WorkflowDefinition } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { moderator } from "./moderator.js";
import type { WorkflowMeta } from "./moderator.js";
import { createCoderRole } from "./roles/coder.js";
import { createWorkspaceCommitterRole } from "./roles/committer.js";
import { createPlannerRole } from "./roles/planner.js";
import { createReviewerRole } from "./roles/reviewer.js";
import { createTesterRole } from "./roles/tester.js";
export type CreateDevelopWorkflowDeps = {
defaultAdapter: AgentFn;
adapters?: Partial<Record<keyof WorkflowMeta, AgentFn>>;
extract: LlmExtractorConfig;
nerveRoot: string;
};
export function createDevelopWorkflowWorkflow({
defaultAdapter,
adapters,
extract,
nerveRoot,
}: CreateDevelopWorkflowDeps): WorkflowDefinition<WorkflowMeta> {
const a = (role: keyof WorkflowMeta) => adapters?.[role] ?? defaultAdapter;
const roles = {
planner: createPlannerRole(a('planner'), extract),
coder: createCoderRole(a('coder'), extract),
reviewer: createReviewerRole(a('reviewer'), extract, { cwd: nerveRoot, conventionsPath: "CONVENTIONS.md" }),
tester: createTesterRole(a('tester'), extract, nerveRoot),
committer: createWorkspaceCommitterRole(a('committer'), extract),
};
return {
name: "develop-workflow",
roles,
moderator,
};
}

View File

@ -1,7 +1,7 @@
import { join } from "node:path"; import { join } from "node:path";
import { createCursorAdapter, cursorAdapter } from "@uncaged/nerve-adapter-cursor"; import { createCursorAdapter, cursorAdapter } from "@uncaged/nerve-adapter-cursor";
import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes";
import { createDevelopWorkflowWorkflow } from "./build.js"; import { createDevelopWorkflowWorkflow } from "@uncaged/nerve-workflow-meta";
const HOME = process.env.HOME ?? "/home/azureuser"; const HOME = process.env.HOME ?? "/home/azureuser";
const NERVE_ROOT = join(HOME, ".uncaged-nerve"); const NERVE_ROOT = join(HOME, ".uncaged-nerve");

View File

@ -1,67 +0,0 @@
import { END } from "@uncaged/nerve-core";
import type { Moderator } from "@uncaged/nerve-core";
import type { PlannerMeta } from "./roles/planner.js";
import type { CoderMeta } from "./roles/coder.js";
import type { ReviewerMeta } from "./roles/reviewer.js";
import type { TesterMeta } from "./roles/tester.js";
import type { CommitterMeta } from "./roles/committer.js";
export type WorkflowMeta = {
planner: PlannerMeta;
coder: CoderMeta;
reviewer: ReviewerMeta;
tester: TesterMeta;
committer: CommitterMeta;
};
const MAX_CODER_ROUNDS = 20;
const MAX_TOTAL_REJECTIONS = 10;
function coderRounds(steps: { role: string }[]): number {
return steps.filter((s) => s.role === "coder").length;
}
function totalRejections(steps: { role: string; meta: unknown }[]): number {
return steps.filter((s) => {
if (s.role === "reviewer") return !(s.meta as Record<string, boolean>).approved;
if (s.role === "tester") return !(s.meta as Record<string, boolean>).passed;
if (s.role === "committer") return !(s.meta as Record<string, boolean>).committed;
return false;
}).length;
}
function canRetryCoder(steps: { role: string; meta: unknown }[]): boolean {
return coderRounds(steps) < MAX_CODER_ROUNDS && totalRejections(steps) < MAX_TOTAL_REJECTIONS;
}
export const moderator: Moderator<WorkflowMeta> = (context) => {
if (context.steps.length === 0) return "planner";
const last = context.steps[context.steps.length - 1];
if (last.role === "planner") {
return last.meta.ready ? "coder" : END;
}
if (last.role === "coder") {
if (last.meta.done) return "reviewer";
return canRetryCoder(context.steps) ? "coder" : END;
}
if (last.role === "reviewer") {
if (last.meta.approved) return "tester";
return canRetryCoder(context.steps) ? "coder" : END;
}
if (last.role === "tester") {
if (last.meta.passed) return "committer";
return canRetryCoder(context.steps) ? "coder" : END;
}
if (last.role === "committer") {
if (last.meta.committed) return END;
return canRetryCoder(context.steps) ? "coder" : END;
}
return END;
};

View File

@ -10,6 +10,7 @@
"@uncaged/nerve-adapter-cursor": "latest", "@uncaged/nerve-adapter-cursor": "latest",
"@uncaged/nerve-adapter-hermes": "latest", "@uncaged/nerve-adapter-hermes": "latest",
"@uncaged/nerve-core": "latest", "@uncaged/nerve-core": "latest",
"@uncaged/nerve-workflow-meta": "link:../../../repos/nerve/packages/workflow-meta",
"@uncaged/nerve-workflow-utils": "latest", "@uncaged/nerve-workflow-utils": "latest",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },

View File

@ -1,69 +0,0 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
export const coderMetaSchema = z.object({
done: z.boolean().describe("true if the workflow files were created and build passes"),
});
export type CoderMeta = z.infer<typeof coderMetaSchema>;
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\`
Also look at existing workflows in the \`workflows/\` directory for patterns.
## Your task
Implement the planner's design. This may be **creating a new workflow** or **modifying an existing one**. If there is reviewer, tester, or committer feedback in the thread, fix the issues they identified.
**IMPORTANT:** The thread contains both the **initial user prompt** (the first message) and the **planner's design**. Read both carefully:
- The **initial prompt** contains the user's specific requirements for role behavior, tools to use, and acceptance criteria
- The **planner's design** contains the architecture, file structure, and routing logic
- When writing role prompts, follow the user's behavioral requirements from the initial prompt do not invent your own interpretation
## Multi-step approach
You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration. For example:
1. First pass: scaffold files / make structural changes
2. Second pass: implement role logic
3. Third pass: fix build/lint errors
## Workflow file structure
Each workflow must have:
- \`workflows/<name>/index.ts\` — WorkflowDefinition default export
- \`workflows/<name>/build.ts\` — factory function
- \`workflows/<name>/moderator.ts\` — moderator + meta types
- \`workflows/<name>/roles/<role>.ts\` — meta schema and prompt function per role
- \`workflows/<name>/package.json\` — with esbuild build script
- \`workflows/<name>/tsconfig.json\` — TypeScript config
For **new workflows**, also update \`nerve.yaml\` with \`workflows.<name>\`.
## Rules
- Keep the WorkflowDefinition<WorkflowMeta> pattern
- No dynamic import()
- Use types (not interfaces)
- Meta should be simple routing signals (single boolean per role)
- Write compile-ready TypeScript
## When to return done: true
Return \`done: true\` ONLY when ALL of the following are true:
- All changes from the plan are implemented
- \`cd workflows/<name> && pnpm install --no-cache && pnpm build\` succeeds (run it!)
- 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.`;
}
export function createCoderRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<CoderMeta> {
return createRole(
adapter,
async (start: StartStep) => coderPrompt({ threadId: start.meta.threadId }),
coderMetaSchema,
extract,
);
}

View File

@ -1,5 +0,0 @@
export {
createCommitterRole as createWorkspaceCommitterRole,
committerMetaSchema,
type CommitterMeta,
} from "@uncaged/nerve-role-committer";

View File

@ -1,65 +0,0 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
export const plannerMetaSchema = z.object({
ready: z.boolean().describe("true if requirements are clear and a workflow can be implemented"),
});
export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
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\`
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/<name>/moderator.ts\`, \`cat workflows/<name>/build.ts\`, \`ls workflows/<name>/roles/\`, 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)
- Roles list (name, purpose, tool)
- Flow transitions / moderator routing logic
- Validation loops design
- External dependencies
- Data flow between roles
For **modifications to existing workflows**:
- Workflow name (existing)
- What changes are needed and why
- Files to add/modify/delete
- Impact on moderator routing logic (this workflow's typical order is planner coder reviewer tester committer)
- Backward compatibility considerations (if any)
**For every role (new or modified)**, include a **Role Behavior** section that describes:
- What the role should do, check, or produce
- What tools or commands it should use
- What criteria determine its meta output (e.g. approved/passed/done)
- Preserve the user's specific requirements verbatim do NOT summarize away details
If requirements are NOT clear, describe what is missing or ambiguous.
End your response with a JSON block:
\`\`\`json
{ "ready": true }
\`\`\`
or
\`\`\`json
{ "ready": false }
\`\`\``;
}
export function createPlannerRole(adapter: AgentFn, extract: LlmExtractorConfig): Role<PlannerMeta> {
return createRole(
adapter,
async (start: StartStep) => plannerPrompt({ threadId: start.meta.threadId }),
plannerMetaSchema,
extract,
);
}

View File

@ -1,3 +0,0 @@
import { createReviewerRole } from "@uncaged/nerve-role-reviewer";
export { createReviewerRole };
export type { ReviewerMeta } from "@uncaged/nerve-role-reviewer";

View File

@ -1,59 +0,0 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
export const testerMetaSchema = z.object({
passed: z.boolean().describe("true if all validation checks passed"),
});
export type TesterMeta = z.infer<typeof testerMetaSchema>;
export function testerPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
return `You are testing a Nerve workflow — either newly created or recently modified.
**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\`
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}/\`):
- \`workflows/<name>/index.ts\`
- \`workflows/<name>/build.ts\`
- \`workflows/<name>/moderator.ts\`
- \`workflows/<name>/roles/\` with one \`.ts\` file per role
- \`workflows/<name>/package.json\`
2. **Build** run inside the workflow directory:
\`\`\`
cd ${nerveRoot}/workflows/<name> && pnpm install --no-cache && pnpm build
\`\`\`
Must produce \`dist/index.js\` without errors.
3. **Config check** \`cd ${nerveRoot} && nerve validate\` passes, confirming nerve.yaml is valid.
4. **Workflow list** \`nerve workflow list\` shows the workflow.
5. **Trigger test** \`nerve workflow trigger <name> --dry-run\` if available, otherwise just confirm the workflow appears in \`nerve workflow status\`.
If any step fails, include the relevant error output.
Output a clear summary: what you checked, what passed, what failed, and why.`;
}
export function createTesterRole(
adapter: AgentFn,
extract: LlmExtractorConfig,
nerveRoot: string,
): Role<TesterMeta> {
return createRole(
adapter,
async (start: StartStep) =>
testerPrompt({ threadId: start.meta.threadId, nerveRoot }),
testerMetaSchema,
extract,
);
}