110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
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<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 buildWorkspaceCommitterRole({
|
|
extract,
|
|
nerveRoot,
|
|
workflowName,
|
|
conventionalCommitScopeHint,
|
|
branchCheckoutExample,
|
|
}: BuildWorkspaceCommitterDeps): Role<CommitterMeta> {
|
|
const innerRole = createRole(
|
|
hermesAdapter,
|
|
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 },
|
|
};
|
|
}
|
|
};
|
|
}
|