小橘 🍊(NEKO Team) 56ce22fb1b Migrate workflows to WorkflowSpec-style roles (RFC-003)
Replace createCursorRole/createHermesRole with adapter + prompt + zod meta.

Add shared compileRoleSpec, cursor ask adapter, nerve.yaml extract defaults.

Refs #248

Made-with: Cursor
2026-04-29 09:23:55 +00:00

63 lines
1.9 KiB
TypeScript

import type { Role, RoleResult, RoleSpec, StartStep, WorkflowMessage } from "@uncaged/nerve-core";
import { createAskCursorAdapter } from "../../../_shared/cursor-ask-adapter.js";
import { compileRoleSpec, zodMeta } from "../../../_shared/rfc003-compile.js";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { z } from "zod";
import { resolveRepoCwd } from "../../lib/repo-context.js";
import { buildPlanPrompt } from "./prompt.js";
export const planMetaSchema = z.object({
ready: z.boolean().describe("true if plan is clear and actionable"),
});
export type PlanMeta = z.infer<typeof planMetaSchema>;
export type BuildPlanDeps = {
provider: LlmProvider;
nerveRoot: string;
};
const CURSOR_TIMEOUT_MS = 300_000;
export function buildPlanRole({ provider, nerveRoot }: BuildPlanDeps): Role<PlanMeta> {
const innerSpec = {
adapter: createAskCursorAdapter({ model: "auto", timeout: CURSOR_TIMEOUT_MS }),
prompt: async (start: StartStep) =>
buildPlanPrompt({
threadId: start.meta.threadId,
nerveRoot,
}),
meta: zodMeta(planMetaSchema),
} satisfies RoleSpec<PlanMeta>;
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<PlanMeta>> => {
const cwd = resolveRepoCwd(messages);
if (cwd === null) {
return {
content: "plan cannot run: missing ---SOLVE_ISSUE_REPO--- or ---SOLVE_ISSUE_PARSE--- in thread",
meta: { ready: false },
};
}
const run = compileRoleSpec(innerSpec, {
provider,
createContext: (s, m) => ({
start: s,
messages: m,
workdir: cwd,
signal: new AbortController().signal,
}),
});
try {
return await run(start, messages);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return {
content: `plan failed: ${msg}`,
meta: { ready: false },
};
}
};
}