nerve-workspace/workflows/_shared/workspace-committer.ts
小橘 1683e41b05 refactor: decouple adapters from workflow factories, roles export createXxxRole
- Rename build* → create* workflow factories
- Workflow factories accept adapters: Record<string, AgentFn>
- Each role file exports createXxxRole(adapter, ...) factory
- _shared/workspace-committer accepts adapter as first param
- All adapter imports moved to index.ts (injection point)
- solve-issue roles also updated

Closes #15
2026-04-29 12:35:07 +00:00

112 lines
3.4 KiB
TypeScript

import type { AgentFn, Role, RoleResult, StartStep } from "@uncaged/nerve-core";
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<typeof committerMetaSchema>;
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/<short-slug>\` or \`feat/<short-slug>\` 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 "<message>"\` (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 <branch-name>\`
**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 createWorkspaceCommitterRole(
adapter: AgentFn,
{
extract,
nerveRoot,
workflowName,
conventionalCommitScopeHint,
branchCheckoutExample,
}: BuildWorkspaceCommitterDeps,
): Role<CommitterMeta> {
const innerRole = createRole(
adapter,
async (start: StartStep) =>
workspaceCommitterPrompt({
threadId: start.meta.threadId,
nerveRoot,
workflowName,
conventionalCommitScopeHint,
branchCheckoutExample,
}),
committerMetaSchema,
extract,
);
return async (start, _messages): Promise<RoleResult<CommitterMeta>> => {
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 },
};
}
};
}