import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import type { Role, RoleResult, WorkflowMessage } from "@uncaged/nerve-core"; import type { SpawnError } from "@uncaged/nerve-workflow-utils"; import { cursorAgent, isDryRun, spawnSafe } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; import type { PlannerMeta } from "../planner/index.js"; import type { TesterMeta } from "../tester/index.js"; import { coderPrompt } from "./prompt.js"; export const coderMetaSchema = z.object({ workflowName: z.string().default(""), attempt: z.number().default(1), files: z .object({ indexTs: z.boolean().default(false), packageJson: z.boolean().default(false), tsconfigJson: z.boolean().default(false), }) .default({ indexTs: false, packageJson: false, tsconfigJson: false }), lintPassed: z.boolean().default(false), buildPassed: z.boolean().default(false), lintLog: z.string().default(""), buildLog: z.string().default(""), cursorOutput: z.string().default(""), reason: z.string().nullable().default(null), }); export type CoderMeta = z.infer; export type BuildCoderDeps = { nerveRoot: string; workflowsDir: string; }; function formatSpawnFailure(error: SpawnError): string { if (error.kind === "spawn_failed") { return error.message; } if (error.kind === "timeout") { return `timeout stdout=${error.stdout.slice(0, 300)} stderr=${error.stderr.slice(0, 300)}`; } return `exit ${error.exitCode} stderr=${error.stderr.slice(0, 500)}`; } function scanGeneratedCodePitfalls(source: string): string[] { const issues: string[] = []; if (/\bawait\s+import\s*\(/.test(source)) { issues.push("Found await import() in generated workflow code"); } if (/\bimport\s*\(\s*["'`]/.test(source) && !source.includes("Dynamic import required")) { issues.push("Found undocumented dynamic import() call"); } if (!/\bexport\s+default\s+/.test(source)) { issues.push("Missing default export of WorkflowDefinition"); } return issues; } async function runLintAndBuild( workflowDir: string, dry: boolean, ): Promise<{ lintPassed: boolean; buildPassed: boolean; lintLog: string; buildLog: string; reason: string | null; }> { const lintRun = await spawnSafe("pnpm", ["run", "check"], { cwd: workflowDir, env: null, timeoutMs: 300_000, dryRun: dry, }); if (!lintRun.ok) { return { lintPassed: false, buildPassed: false, lintLog: formatSpawnFailure(lintRun.error), buildLog: "", reason: `lint failed: ${formatSpawnFailure(lintRun.error)}`, }; } const lintLog = lintRun.value.stderr.trim() || lintRun.value.stdout.trim() || "(no output)"; const tscRun = await spawnSafe("npx", ["tsc", "--noEmit"], { cwd: workflowDir, env: null, timeoutMs: 300_000, dryRun: dry, }); if (!tscRun.ok) { return { lintPassed: true, buildPassed: false, lintLog, buildLog: formatSpawnFailure(tscRun.error), reason: `build failed: ${formatSpawnFailure(tscRun.error)}`, }; } const buildLog = tscRun.value.stderr.trim() || tscRun.value.stdout.trim() || "(no output)"; return { lintPassed: true, buildPassed: true, lintLog, buildLog, reason: null }; } function lastMetaForRole(messages: WorkflowMessage[], role: string): M | null { for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === role) { return messages[i].meta as M; } } return null; } export function buildCoderRole({ nerveRoot, workflowsDir }: BuildCoderDeps): Role { return async (start, messages) => { const dry = isDryRun(start); const plannerMeta = lastMetaForRole(messages, "planner"); const previousTester = lastMetaForRole(messages, "tester"); const attempt = messages.filter((m) => m.role === "coder").length + 1; if (plannerMeta === null || plannerMeta.workflowName.trim().length === 0) { return { content: "coder cannot continue: missing planner output", meta: { workflowName: "", attempt, files: { indexTs: false, packageJson: false, tsconfigJson: false }, lintPassed: false, buildPassed: false, lintLog: "", buildLog: "", cursorOutput: "", reason: "missing planner output", }, } satisfies RoleResult; } const wfName = plannerMeta.workflowName.trim(); const feedback = previousTester !== null && previousTester.passed === false ? `\n\nPrevious tester failure to fix:\n${previousTester.reason}\n${previousTester.dryRunLog}\n` : ""; const prompt = coderPrompt({ workflowsDir, wfName, planMarkdown: plannerMeta.planMarkdown, plannerStructured: { workflowName: plannerMeta.workflowName, roles: plannerMeta.roles, flowTransitions: plannerMeta.flowTransitions, validationLoopsDesign: plannerMeta.validationLoopsDesign, externalDeps: plannerMeta.externalDeps, dataFlow: plannerMeta.dataFlow, }, feedback, nerveRoot, }); const agentRun = await cursorAgent({ prompt, mode: "default", cwd: nerveRoot, env: null, timeoutMs: null, dryRun: dry, }); const workflowDir = join(workflowsDir, wfName); const files = { indexTs: existsSync(join(workflowDir, "index.ts")), packageJson: existsSync(join(workflowDir, "package.json")), tsconfigJson: existsSync(join(workflowDir, "tsconfig.json")), }; const missing = [ files.indexTs ? null : "index.ts", files.packageJson ? null : "package.json", files.tsconfigJson ? null : "tsconfig.json", ].filter((x) => x !== null) as string[]; if (!agentRun.ok) { return { content: `coder failed: ${formatSpawnFailure(agentRun.error)}`, meta: { workflowName: wfName, attempt, files, lintPassed: false, buildPassed: false, lintLog: "", buildLog: "", cursorOutput: "", reason: formatSpawnFailure(agentRun.error), }, } satisfies RoleResult; } if (missing.length > 0) { return { content: `coder failed: missing required files (${missing.join(", ")})`, meta: { workflowName: wfName, attempt, files, lintPassed: false, buildPassed: false, lintLog: "", buildLog: "", cursorOutput: agentRun.value, reason: `missing files: ${missing.join(", ")}`, }, } satisfies RoleResult; } const source = readFileSync(join(workflowDir, "index.ts"), "utf-8"); const pitfalls = scanGeneratedCodePitfalls(source); if (pitfalls.length > 0) { return { content: `coder static check failed:\n${pitfalls.join("\n")}`, meta: { workflowName: wfName, attempt, files, lintPassed: false, buildPassed: false, lintLog: pitfalls.join("\n"), buildLog: "", cursorOutput: agentRun.value, reason: pitfalls.join("; "), }, } satisfies RoleResult; } const check = await runLintAndBuild(workflowDir, dry); const passed = check.lintPassed && check.buildPassed; return { content: passed ? `coder PASS: lint+build ok\n\n${check.lintLog}\n\n${check.buildLog}` : `coder FAIL: ${check.reason ?? "unknown error"}`, meta: { workflowName: wfName, attempt, files, lintPassed: check.lintPassed, buildPassed: check.buildPassed, lintLog: check.lintLog, buildLog: check.buildLog, cursorOutput: agentRun.value, reason: check.reason, }, } satisfies RoleResult; }; }