feat: add @uncaged/nerve-role-committer package (RFC-004 Phase 1)

First shared role package. Extracts workspace committer into a
reusable package with decorator chain (withDryRun + onFail).

Also fixes workflow-utils test types (StartStep shape, TestMeta constraint).
This commit is contained in:
2026-04-29 14:07:26 +00:00
parent 3644cce2c8
commit 915584ff11
6 changed files with 142 additions and 3 deletions
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@uncaged/nerve-role-committer",
"version": "0.5.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"publishConfig": {
"access": "public"
},
"scripts": {
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
"build": "rslib build",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@uncaged/nerve-core": "workspace:*",
"@uncaged/nerve-workflow-utils": "workspace:*",
"zod": "^4.3.6"
},
"devDependencies": {
"@rslib/core": "^0.21.3",
"typescript": "^5.8.3",
"vitest": "^4.1.5"
}
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from "@rslib/core";
export default defineConfig({
lib: [
{
format: "esm",
dts: true,
},
],
source: {
entry: {
index: "src/index.ts",
},
},
output: {
target: "node",
cleanDistPath: true,
},
});
+60
View File
@@ -0,0 +1,60 @@
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
import {
createRole,
decorateRole,
withDryRun,
onFail,
} 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 committerPrompt(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 }
\`\`\``;
}
/**
* Creates a committer role that branches, commits, and pushes changes.
* The agent reads the workflow thread to infer branch name, scope, and commit message.
*/
export function createCommitterRole(
adapter: AgentFn,
extract: LlmExtractorConfig,
): Role<CommitterMeta> {
const inner = createRole(
adapter,
async (start: StartStep) => committerPrompt(start.meta.threadId),
committerMetaSchema,
extract,
);
return decorateRole(inner, [
withDryRun({ label: "committer", meta: { committed: true } as CommitterMeta }),
onFail({ label: "committer", meta: { committed: false } as CommitterMeta }),
]) as Role<CommitterMeta>;
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"composite": false
},
"include": ["src"]
}
@@ -7,18 +7,21 @@ import type {
WorkflowMessage,
} from "@uncaged/nerve-core";
import { START } from "@uncaged/nerve-core";
import { decorateRole, onFail, withDryRun } from "../role-decorators.js";
type TestMeta = { ok: boolean };
type TestMeta = Record<string, unknown> & { ok: boolean };
function fakeStart(dryRun: boolean): StartStep {
return {
role: "test",
role: START,
content: "",
meta: {
threadId: "t1",
dryRun,
startedAt: new Date().toISOString(),
maxRounds: 10,
},
timestamp: Date.now(),
};
}
+22
View File
@@ -156,6 +156,28 @@ importers:
specifier: ^4.14.0
version: 4.85.0(@cloudflare/workers-types@4.20260425.1)
packages/role-committer:
dependencies:
'@uncaged/nerve-core':
specifier: workspace:*
version: link:../core
'@uncaged/nerve-workflow-utils':
specifier: workspace:*
version: link:../workflow-utils
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
'@rslib/core':
specifier: ^0.21.3
version: 0.21.3(typescript@5.9.3)
typescript:
specifier: ^5.8.3
version: 5.9.3
vitest:
specifier: ^4.1.5
version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
packages/skills: {}
packages/store: