小橘 57c740cdde Revert "chore(workflow): auto-generated commit"
This reverts commit 75f2768a8c7713879bb2ab564f42f24bc609338e.
2026-04-28 15:49:22 +00:00

163 lines
5.3 KiB
TypeScript

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<ImplementerMeta> {
return async (_start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<ImplementerMeta>> => {
const plannerMeta = lastMetaForRole<PlannerMeta>(messages, "planner");
const intakeMeta = lastMetaForRole<IntakeMeta>(messages, "intake");
const issueMeta = lastMetaForRole<IssueReaderMeta>(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,
},
};
};
}