import type { AgentFn, Role, RoleResult, ThreadContext, WorkflowMessage } from "@uncaged/nerve-core"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; import { resolveRepoCwd } from "../lib/repo-context.js"; function buildPlanPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string { return `You are the **plan** agent (analysis only — ask mode). You produce an implementation plan for fixing the issue. Read workflow context: \`nerve thread show ${threadId}\` Read Nerve workspace conventions (coding rules for agents): \`cat ${nerveRoot}/CONVENTIONS.md\` In the **target repository** (your cwd), skim relevant files and read \`CONVENTIONS.md\` **if it exists** there. ## Output Write an implementation plan in **markdown** with: 1. Problem understanding 2. Change strategy 3. Target files (paths) 4. **Test commands** to run (explicit shell commands, e.g. \`pnpm test\`, \`pnpm vitest run\`) 5. Risks End your reply with a JSON code block (meta signal): \`\`\`json { "ready": true } \`\`\` Use \`{ "ready": false }\` if the plan cannot be made actionable. **ready=true** only when the plan is clear and actionable.`; } export const planMetaSchema = z.object({ ready: z.boolean().describe("true if plan is clear and actionable"), }); export type PlanMeta = z.infer; export type CreatePlanRoleDeps = { extract: LlmExtractorConfig; nerveRoot: string; }; export function createPlanRole( adapter: AgentFn, { extract, nerveRoot }: CreatePlanRoleDeps, ): Role { return async (ctx: ThreadContext): Promise> => { const messages = ctx.steps as unknown as WorkflowMessage[]; 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 innerRole = createRole( adapter, async (innerCtx: ThreadContext) => buildPlanPrompt({ threadId: innerCtx.start.meta.threadId, nerveRoot, }), planMetaSchema, extract, ); const innerCtx: ThreadContext = { ...ctx, start: { ...ctx.start, meta: { ...ctx.start.meta, workdir: cwd }, }, }; try { return await innerRole(innerCtx); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { content: `plan failed: ${msg}`, meta: { ready: false }, }; } }; }