import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import { cursorAgent, llmExtract } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; import { readNerveConfigText } from "../../lib/nerve-read.js"; import { resolveDashScopeProvider } from "../../lib/provider.js"; import { lastMetaForRole } from "../../lib/meta-helpers.js"; import { formatSpawnFailure } from "../../lib/spawn-utils.js"; import type { IssueReaderMeta } from "../issue-reader/index.js"; import { buildPlannerPrompt } from "./prompt.js"; export type PlannerMeta = { plan: string | null; targetFiles: string[] | null; testCommands: string[] | null; riskNotes: string[] | null; planningOk: boolean; failureKind: "none" | "planning_failed"; failureReason: string | null; }; const planSchema = z.object({ targetFiles: z.array(z.string().default("")).default([]), testCommands: z.array(z.string().default("")).default([]), riskNotes: z.array(z.string().default("")).default([]), }); export type BuildPlannerDeps = { nerveRoot: string; }; export function buildPlannerRole({ nerveRoot }: BuildPlannerDeps): Role { return async (_start: StartStep, messages: WorkflowMessage[]): Promise> => { const issueMeta = lastMetaForRole(messages, "issue-reader"); if (issueMeta === null || !issueMeta.fetchOk || issueMeta.issue === null) { return { content: "planner cannot continue: issue-reader has no issue data", meta: { plan: null, targetFiles: null, testCommands: null, riskNotes: null, planningOk: false, failureKind: "planning_failed", failureReason: "missing issue data", }, }; } const prompt = buildPlannerPrompt({ issue: issueMeta.issue, nerveYamlText: readNerveConfigText(nerveRoot), }); const result = await cursorAgent({ prompt, mode: "ask", model: "auto", cwd: nerveRoot, env: null, timeoutMs: null, }); if (!result.ok) { return { content: `planner failed: ${formatSpawnFailure(result.error)}`, meta: { plan: null, targetFiles: null, testCommands: null, riskNotes: null, planningOk: false, failureKind: "planning_failed", failureReason: formatSpawnFailure(result.error), }, }; } const plan = result.value; const provider = await resolveDashScopeProvider(nerveRoot); if (provider === null) { return { content: plan, meta: { plan, targetFiles: null, testCommands: null, riskNotes: null, planningOk: true, failureKind: "none", failureReason: null, }, }; } const structured = await llmExtract({ text: plan, schema: planSchema, provider }); if (!structured.ok) { return { content: `${plan}\n\n[llmExtract error] ${JSON.stringify(structured.error)}`, meta: { plan, targetFiles: null, testCommands: null, riskNotes: null, planningOk: true, failureKind: "none", failureReason: null, }, }; } return { content: plan, meta: { plan, targetFiles: structured.value.targetFiles, testCommands: structured.value.testCommands, riskNotes: structured.value.riskNotes, planningOk: true, failureKind: "none", failureReason: null, }, }; }; }