refactor: use @uncaged/nerve-role-committer package #19

Merged
xiaoju merged 1 commits from feat/use-role-committer-package into master 2026-04-29 14:20:02 +00:00
6 changed files with 20 additions and 93 deletions

View File

@ -13,7 +13,8 @@
"@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon", "@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon",
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils", "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
"drizzle-orm": "latest", "drizzle-orm": "latest",
"zod": "^4.3.6" "zod": "^4.3.6",
"@uncaged/nerve-role-committer": "link:../repos/nerve/packages/role-committer"
}, },
"devDependencies": { "devDependencies": {
"drizzle-kit": "latest" "drizzle-kit": "latest"
@ -27,7 +28,8 @@
"@uncaged/nerve-adapter-hermes": "link:../repos/nerve/packages/adapter-hermes", "@uncaged/nerve-adapter-hermes": "link:../repos/nerve/packages/adapter-hermes",
"@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon", "@uncaged/nerve-daemon": "link:../repos/nerve/packages/daemon",
"@uncaged/nerve-core": "link:../repos/nerve/packages/core", "@uncaged/nerve-core": "link:../repos/nerve/packages/core",
"@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils" "@uncaged/nerve-workflow-utils": "link:../repos/nerve/packages/workflow-utils",
"@uncaged/nerve-role-committer": "link:../repos/nerve/packages/role-committer"
} }
} }
} }

4
pnpm-lock.yaml generated
View File

@ -10,6 +10,7 @@ overrides:
'@uncaged/nerve-daemon': link:../repos/nerve/packages/daemon '@uncaged/nerve-daemon': link:../repos/nerve/packages/daemon
'@uncaged/nerve-core': link:../repos/nerve/packages/core '@uncaged/nerve-core': link:../repos/nerve/packages/core
'@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils '@uncaged/nerve-workflow-utils': link:../repos/nerve/packages/workflow-utils
'@uncaged/nerve-role-committer': link:../repos/nerve/packages/role-committer
importers: importers:
@ -27,6 +28,9 @@ importers:
'@uncaged/nerve-daemon': '@uncaged/nerve-daemon':
specifier: link:../repos/nerve/packages/daemon specifier: link:../repos/nerve/packages/daemon
version: link:../repos/nerve/packages/daemon version: link:../repos/nerve/packages/daemon
'@uncaged/nerve-role-committer':
specifier: link:../repos/nerve/packages/role-committer
version: link:../repos/nerve/packages/role-committer
'@uncaged/nerve-workflow-utils': '@uncaged/nerve-workflow-utils':
specifier: link:../repos/nerve/packages/workflow-utils specifier: link:../repos/nerve/packages/workflow-utils
version: link:../repos/nerve/packages/workflow-utils version: link:../repos/nerve/packages/workflow-utils

View File

@ -1,65 +0,0 @@
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>;
function workspaceCommitterPrompt(threadId: string): string {
return `You are the committer agent. The coder finished with a passing build; your job is to branch, commit, and push.
1. Read the workflow thread: \`nerve thread show ${threadId}\` — understand what was planned, coded, and reviewed.
2. Run \`git status\`. If nothing to commit, set committed=false.
3. Create a feature branch: infer a good \`fix/<slug>\` or \`feat/<slug>\` name from the thread context.
4. \`git add -A\`
5. Write a conventional commit message based on the thread context.
6. \`git commit -m "<message>"\` — do NOT pass \`--author\`, use repo git config.
7. \`git push -u origin <branch>\`
**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: LlmExtractorConfig,
): Role<CommitterMeta> {
const innerRole = createRole(
adapter,
async (start: StartStep) => workspaceCommitterPrompt(start.meta.threadId),
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 },
};
}
try {
return await innerRole(start, _messages);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return {
content: `committer failed: ${msg}`,
meta: { committed: false },
};
}
};
}

View File

@ -1,5 +1,5 @@
export { export {
createWorkspaceCommitterRole, createCommitterRole as createWorkspaceCommitterRole,
committerMetaSchema, committerMetaSchema,
type CommitterMeta, type CommitterMeta,
} from "../../_shared/workspace-committer.js"; } from "@uncaged/nerve-role-committer";

View File

@ -1,5 +1,5 @@
export { export {
createWorkspaceCommitterRole, createCommitterRole as createWorkspaceCommitterRole,
committerMetaSchema, committerMetaSchema,
type CommitterMeta, type CommitterMeta,
} from "../../_shared/workspace-committer.js"; } from "@uncaged/nerve-role-committer";

View File

@ -1,6 +1,6 @@
import type { AgentFn, Role, RoleResult, StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils"; import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import { createRole, isDryRun } from "@uncaged/nerve-workflow-utils"; import { createRole, decorateRole, withDryRun, onFail } from "@uncaged/nerve-workflow-utils";
import { z } from "zod"; import { z } from "zod";
import { committerPrompt } from "./prompt.js"; import { committerPrompt } from "./prompt.js";
@ -16,29 +16,15 @@ export function createCommitterRole(
adapter: AgentFn, adapter: AgentFn,
extract: LlmExtractorConfig, extract: LlmExtractorConfig,
): Role<CommitterMeta> { ): Role<CommitterMeta> {
const innerRole = createRole( const inner = createRole(
adapter, adapter,
async (start: StartStep) => committerPrompt({ threadId: start.meta.threadId }), async (start: StartStep) => committerPrompt({ threadId: start.meta.threadId }),
committerMetaSchema, committerMetaSchema,
extract, extract,
); );
return async (start: StartStep, messages: WorkflowMessage[]): Promise<RoleResult<CommitterMeta>> => { return decorateRole(inner, [
if (isDryRun(start)) { withDryRun({ label: "committer", meta: { committed: true } as CommitterMeta }),
return { onFail({ label: "committer", meta: { committed: false } as CommitterMeta }),
content: "[dry-run] committer skipped (no git branch/commit/push)", ]) as Role<CommitterMeta>;
meta: { committed: true },
};
}
try {
return await innerRole(start, messages);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
return {
content: `committer failed: ${msg}`,
meta: { committed: false },
};
}
};
} }