import type { Role, RoleResult, StartStep } 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"; 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 BuildWorkspaceCommitterDeps = { extract: LlmExtractorConfig; nerveRoot: string; workflowName: string; conventionalCommitScopeHint: string; branchCheckoutExample: string; }; function workspaceCommitterPrompt({ threadId, nerveRoot, workflowName, conventionalCommitScopeHint, branchCheckoutExample, }: { threadId: string; nerveRoot: string; workflowName: string; conventionalCommitScopeHint: string; branchCheckoutExample: string; }): string { return `You are the **committer** agent (Hermes) for the **${workflowName}** workflow. The coder finished with a passing build; you branch, commit, and push workspace changes. ## Context 1. Read the workflow thread: \`nerve thread show ${threadId}\` 2. Your git repository root is: \`${nerveRoot}\` — \`cd\` there for all git commands. ## Steps (in order) 1. Run \`git status\`. There should be uncommitted changes from the coder. If there is nothing to commit, set **committed** to false and explain. 2. Create a short-lived branch (do not commit directly on the default branch if it would mix unrelated work): - Prefer \`fix/\` or \`feat/\` with a lowercase hyphenated slug from the thread (planner/coder context). - Example: \`${branchCheckoutExample}\` 3. \`git add -A\` 4. Write a **conventional commit** message summarizing what changed and why (scope may be \`${conventionalCommitScopeHint}\` or similar). 5. \`git commit -m ""\` (use multiple \`-m\` if you need a body). Do **not** pass \`--author\` — always use the repo's local git config identity. 6. \`git push -u origin \` **committed=true** only if branch was created, commit succeeded, and **push** succeeded. End your reply with a JSON line: \`\`\`json { "committed": true } \`\`\` or \`\`\`json { "committed": false } \`\`\``; } export function buildWorkspaceCommitterRole({ extract, nerveRoot, workflowName, conventionalCommitScopeHint, branchCheckoutExample, }: BuildWorkspaceCommitterDeps): Role { const innerRole = createRole( hermesAdapter, async (start: StartStep) => workspaceCommitterPrompt({ threadId: start.meta.threadId, nerveRoot, workflowName, conventionalCommitScopeHint, branchCheckoutExample, }), committerMetaSchema, extract, ); return async (start, _messages): Promise> => { if (isDryRun(start)) { return { content: "[dry-run] committer skipped (no git branch/commit/push)", meta: { committed: true }, }; } const innerStart = { ...start, meta: { ...start.meta, workdir: nerveRoot }, } 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 }, }; } }; }