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:
parent
b0cff7e0ed
commit
60979aaa6a
@ -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
10
pnpm-lock.yaml
generated
@ -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
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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");
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export {
|
|
||||||
createCommitterRole as createWorkspaceCommitterRole,
|
|
||||||
committerMetaSchema,
|
|
||||||
type CommitterMeta,
|
|
||||||
} from "@uncaged/nerve-role-committer";
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { createReviewerRole } from "@uncaged/nerve-role-reviewer";
|
|
||||||
export { createReviewerRole };
|
|
||||||
export type { ReviewerMeta } from "@uncaged/nerve-role-reviewer";
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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");
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export {
|
|
||||||
createCommitterRole as createWorkspaceCommitterRole,
|
|
||||||
committerMetaSchema,
|
|
||||||
type CommitterMeta,
|
|
||||||
} from "@uncaged/nerve-role-committer";
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { createReviewerRole } from "@uncaged/nerve-role-reviewer";
|
|
||||||
export { createReviewerRole };
|
|
||||||
export type { ReviewerMeta } from "@uncaged/nerve-role-reviewer";
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user