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 { committerPrompt } from "./prompt.js"; export const committerMetaSchema = z.object({ invoked: z.boolean().default(false), success: z.boolean().default(false), branch: z.string().nullable().default(null), commitHash: z.string().nullable().default(null), pushed: z.boolean().nullable().default(null), log: z.string().default(""), error: z.string().nullable().default(null), }); export type CommitterMeta = z.infer; export type BuildCommitterDeps = { nerveRoot: 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 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; } function inferWorkflowName(messages: WorkflowMessage[]): string { const tester = lastMetaForRole(messages, "tester"); if (tester !== null && tester.workflowName.trim().length > 0) { return tester.workflowName.trim(); } const coder = lastMetaForRole<{ workflowName: string }>(messages, "coder"); if (coder !== null && coder.workflowName.trim().length > 0) { return coder.workflowName.trim(); } const planner = lastMetaForRole(messages, "planner"); if (planner !== null && planner.workflowName.trim().length > 0) { return planner.workflowName.trim(); } return ""; } async function runHermesCommitter( task: string, nerveRoot: string, ): Promise { const commandAttempts: Array<{ cmd: string; args: string[] }> = [ { cmd: "hermes-agent", args: ["--cwd", nerveRoot, "--task", task] }, { cmd: "hermes", args: ["agent", "--cwd", nerveRoot, "--task", task] }, ]; for (const candidate of commandAttempts) { const run = await spawnSafe(candidate.cmd, candidate.args, { cwd: nerveRoot, env: null, timeoutMs: 600_000, dryRun: false, }); if (!run.ok) { continue; } const text = `${run.value.stdout}\n${run.value.stderr}`; const branch = text.match(/^BRANCH=(.*)$/m)?.[1]?.trim() ?? null; const commitHash = text.match(/^COMMIT=(.*)$/m)?.[1]?.trim() ?? null; const pushedText = text.match(/^PUSHED=(.*)$/m)?.[1]?.trim().toLowerCase() ?? "unknown"; const pushed = pushedText === "true" ? true : pushedText === "false" ? false : null; return { invoked: true, success: true, branch: branch && branch.length > 0 ? branch : null, commitHash: commitHash && commitHash.length > 0 ? commitHash : null, pushed, log: text.slice(0, 20_000), error: null, }; } const fallback = await cursorAgent({ prompt: `Run this git committer task in repository ${nerveRoot}:\n\n${task}`, mode: "default", cwd: nerveRoot, env: null, timeoutMs: null, dryRun: false, }); if (!fallback.ok) { return { invoked: true, success: false, branch: null, commitHash: null, pushed: null, log: "", error: `hermes and fallback both failed: ${formatSpawnFailure(fallback.error)}`, }; } const out = fallback.value; const branch = out.match(/(?:branch|BRANCH)\s*[:=]\s*([^\s]+)/)?.[1] ?? null; const commitHash = out.match(/[a-f0-9]{7,40}/)?.[0] ?? null; return { invoked: true, success: true, branch, commitHash, pushed: out.toLowerCase().includes("push") ? true : null, log: out.slice(0, 20_000), error: null, }; } export function buildCommitterRole({ nerveRoot }: BuildCommitterDeps): Role { return async (start, messages) => { const dry = isDryRun(start); const planner = lastMetaForRole(messages, "planner"); const tester = lastMetaForRole(messages, "tester"); const workflowName = inferWorkflowName(messages); const skipMeta: CommitterMeta = { invoked: false, success: false, branch: null, commitHash: null, pushed: null, log: "", error: null, }; if (planner === null || tester === null || workflowName.length === 0) { return { content: "committer skipped: missing planner/tester/workflowName context", meta: { ...skipMeta, error: "missing committer context" }, } satisfies RoleResult; } if (!tester.passed) { return { content: "committer skipped: tester not passed", meta: { ...skipMeta, error: "tester not passed" }, } satisfies RoleResult; } if (dry) { return { content: "[dry-run] skipped hermes committer", meta: { invoked: true, success: true, branch: "wf/dry-run", commitHash: null, pushed: null, log: "[dry-run] skipped hermes committer", error: null, }, } satisfies RoleResult; } const task = committerPrompt({ nerveRoot, workflowName, userPrompt: planner.userPrompt, testerReason: tester.reason, }); const committed = await runHermesCommitter(task, nerveRoot); return { content: committed.success ? committed.log : `committer failed: ${committed.error ?? "unknown"}`, meta: committed, } satisfies RoleResult; }; }