import type { Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import { hermesAdapter } from "@uncaged/nerve-adapter-hermes"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; import { z } from "zod"; import { resolveRepoCwd } from "../../lib/repo-context.js"; import { committerPrompt } from "./prompt.js"; export const committerMetaSchema = z.object({ committed: z .boolean() .describe("true if branch created, changes committed, and pushed successfully"), }); export type CommitterMeta = z.infer; export type BuildCommitterDeps = { extract: LlmExtractorConfig; nerveRoot: string; }; export function buildCommitterRole({ extract, nerveRoot }: BuildCommitterDeps): Role { const innerRole = createRole( hermesAdapter, async (start: StartStep) => committerPrompt({ threadId: start.meta.threadId, nerveRoot, }), committerMetaSchema, extract, ); return async (start: StartStep, messages: WorkflowMessage[]): Promise> => { const cwd = resolveRepoCwd(messages); if (cwd === null) { return { content: "committer cannot run: missing repo path in thread markers", meta: { committed: false }, }; } if (isDryRun(start)) { return { content: "[dry-run] committer skipped (no git branch/commit/push)", meta: { committed: true }, }; } const innerStart = { ...start, meta: { ...start.meta, workdir: cwd }, } as StartStep; try { return await innerRole(innerStart, messages); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { content: `committer failed: ${msg}`, meta: { committed: false }, }; } }; }