import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import { cursorAgent, spawnSafe } from "@uncaged/nerve-workflow-utils"; import { lastMetaForRole } from "../../lib/meta-helpers.js"; import { formatSpawnFailure } from "../../lib/spawn-utils.js"; import { slugify } from "../../lib/text-utils.js"; import type { IntakeMeta } from "../intake/index.js"; import type { IssueReaderMeta } from "../issue-reader/index.js"; import type { PlannerMeta } from "../planner/index.js"; import { buildImplementerPrompt } from "./prompt.js"; export type ImplementerMeta = { branchName: string | null; changedFiles: string[] | null; implementationOk: boolean; attempt: number; failureKind: "none" | "branch_failed" | "agent_failed" | "no_diff"; failureReason: string | null; implementationLog: string | null; }; export type BuildImplementerDeps = { nerveRoot: string; }; export function buildImplementerRole({ nerveRoot }: BuildImplementerDeps): Role { return async (_start: StartStep, messages: WorkflowMessage[]): Promise> => { const plannerMeta = lastMetaForRole(messages, "planner"); const intakeMeta = lastMetaForRole(messages, "intake"); const issueMeta = lastMetaForRole(messages, "issue-reader"); const attempt = messages.filter((message) => message.role === "implementer").length + 1; if ( plannerMeta === null || !plannerMeta.planningOk || plannerMeta.plan === null || intakeMeta === null || intakeMeta.issueNumber === null ) { return { content: "implementer cannot continue: missing planner or intake context", meta: { branchName: null, changedFiles: null, implementationOk: false, attempt, failureKind: "agent_failed", failureReason: "missing planner/intake context", implementationLog: null, }, }; } const slugSource = issueMeta?.issue?.title ?? plannerMeta.plan.split("\n")[0] ?? "fix"; const branchName = `fix/issue-${intakeMeta.issueNumber}-${slugify(slugSource)}`; const existsResult = await spawnSafe("git", ["rev-parse", "--verify", `refs/heads/${branchName}`], { cwd: nerveRoot, env: null, timeoutMs: 10_000, }); const checkoutResult = existsResult.ok ? await spawnSafe("git", ["checkout", branchName], { cwd: nerveRoot, env: null, timeoutMs: 20_000 }) : await spawnSafe("git", ["checkout", "-b", branchName], { cwd: nerveRoot, env: null, timeoutMs: 20_000 }); if (!checkoutResult.ok) { return { content: `branch setup failed: ${formatSpawnFailure(checkoutResult.error)}`, meta: { branchName, changedFiles: null, implementationOk: false, attempt, failureKind: "branch_failed", failureReason: formatSpawnFailure(checkoutResult.error), implementationLog: null, }, }; } const prompt = buildImplementerPrompt({ issueNumber: intakeMeta.issueNumber, plan: plannerMeta.plan, targetFilesText: plannerMeta.targetFiles?.join("\n") ?? "(not specified)", testCommandsText: plannerMeta.testCommands?.join("\n") ?? "(not specified)", }); const agentResult = await cursorAgent({ prompt, mode: "default", model: "auto", cwd: nerveRoot, env: null, timeoutMs: null, }); if (!agentResult.ok) { return { content: `implementer agent failed: ${formatSpawnFailure(agentResult.error)}`, meta: { branchName, changedFiles: null, implementationOk: false, attempt, failureKind: "agent_failed", failureReason: formatSpawnFailure(agentResult.error), implementationLog: null, }, }; } const diffResult = await spawnSafe("git", ["diff", "--name-only"], { cwd: nerveRoot, env: null, timeoutMs: 15_000, }); if (!diffResult.ok) { return { content: `implementation finished but diff check failed: ${formatSpawnFailure(diffResult.error)}`, meta: { branchName, changedFiles: null, implementationOk: false, attempt, failureKind: "no_diff", failureReason: formatSpawnFailure(diffResult.error), implementationLog: agentResult.value, }, }; } const changedFiles = diffResult.value.stdout .split("\n") .map((file) => file.trim()) .filter((file) => file.length > 0); if (changedFiles.length === 0) { return { content: "implementer made no local diff", meta: { branchName, changedFiles: [], implementationOk: false, attempt, failureKind: "no_diff", failureReason: "git diff --name-only is empty", implementationLog: agentResult.value, }, }; } return { content: `branch ready: ${branchName}\nchanged files:\n${changedFiles.join("\n")}`, meta: { branchName, changedFiles, implementationOk: true, attempt, failureKind: "none", failureReason: null, implementationLog: agentResult.value, }, }; }; }