feat: add @uncaged/nerve-role-reviewer package
Extract shared reviewer role (diff analysis, convention checking) into a reusable package. Identical logic currently duplicated across develop-sense and develop-workflow workspaces. Refs RFC-004 Phase 1
This commit is contained in:
@@ -1,11 +1,6 @@
|
|||||||
import type { AgentFn, Role, StartStep } 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 {
|
import { createRole, decorateRole, onFail, withDryRun } from "@uncaged/nerve-workflow-utils";
|
||||||
createRole,
|
|
||||||
decorateRole,
|
|
||||||
withDryRun,
|
|
||||||
onFail,
|
|
||||||
} from "@uncaged/nerve-workflow-utils";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const committerMetaSchema = z.object({
|
export const committerMetaSchema = z.object({
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/nerve-role-reviewer",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { createReviewerRole, reviewerMetaSchema } from "./reviewer.js";
|
||||||
|
export type { ReviewerMeta, ReviewerConfig } from "./reviewer.js";
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type { AgentFn, Role, StartStep } from "@uncaged/nerve-core";
|
||||||
|
import type { LlmExtractorConfig } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { createRole } from "@uncaged/nerve-workflow-utils";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const reviewerMetaSchema = z.object({
|
||||||
|
approved: z.boolean().describe("true if the diff is clean and ready to merge"),
|
||||||
|
});
|
||||||
|
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
|
||||||
|
|
||||||
|
export type ReviewerConfig = {
|
||||||
|
/** Working directory of the project */
|
||||||
|
cwd: string;
|
||||||
|
/** Path to conventions/standards file, relative to cwd. Default: "CONVENTIONS.md" */
|
||||||
|
conventionsPath: string | null;
|
||||||
|
/** Extra checklist items appended to the review criteria */
|
||||||
|
extraChecks: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaults: ReviewerConfig = {
|
||||||
|
cwd: ".",
|
||||||
|
conventionsPath: "CONVENTIONS.md",
|
||||||
|
extraChecks: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function reviewerPrompt({
|
||||||
|
threadId,
|
||||||
|
config,
|
||||||
|
}: { threadId: string; config: ReviewerConfig }): string {
|
||||||
|
const { cwd, conventionsPath, extraChecks } = config;
|
||||||
|
|
||||||
|
const conventionsBlock = conventionsPath
|
||||||
|
? `Read project conventions: \`cat ${cwd}/${conventionsPath}\`\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const extraBlock =
|
||||||
|
extraChecks.length > 0
|
||||||
|
? `\n### 📋 Project-specific checks\n${extraChecks.map((c) => `- ${c}`).join("\n")}\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `You are a **code reviewer**. You run after the coder and before the tester.
|
||||||
|
|
||||||
|
**IMPORTANT: The project is at \`${cwd}\`. Always \`cd ${cwd}\` first.**
|
||||||
|
|
||||||
|
Read the workflow thread for context: \`nerve thread ${threadId}\`
|
||||||
|
${conventionsBlock}
|
||||||
|
## Your job — static analysis of the git diff
|
||||||
|
|
||||||
|
Run these commands and analyze the output:
|
||||||
|
|
||||||
|
1. **\`cd ${cwd} && git diff --stat\`** — see what files changed
|
||||||
|
2. **\`cd ${cwd} && git diff\`** — read the actual diff
|
||||||
|
3. **\`cd ${cwd} && git status --short\`** — check for untracked files
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
### 🔴 Reject (approved: false) — tell coder exactly what to fix
|
||||||
|
- **Garbage files**: build artifacts, lockfiles, IDE config that shouldn't be committed
|
||||||
|
- **Secrets/credentials**: API keys, tokens, passwords hardcoded in the diff
|
||||||
|
- **Unrelated changes**: files modified outside the scope of the task
|
||||||
|
${conventionsPath ? `- **Convention violations**: patterns that contradict ${conventionsPath}\n` : ""}${extraBlock}
|
||||||
|
### ✅ Approve (approved: true) — no comment needed
|
||||||
|
- Diff is clean, focused, follows project standards
|
||||||
|
|
||||||
|
End with:
|
||||||
|
\`\`\`json
|
||||||
|
{ "approved": true }
|
||||||
|
\`\`\`
|
||||||
|
or
|
||||||
|
\`\`\`json
|
||||||
|
{ "approved": false }
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reviewer role that performs static analysis on git diffs.
|
||||||
|
* Checks for garbage files, secrets, unrelated changes, and project conventions.
|
||||||
|
*/
|
||||||
|
export function createReviewerRole(
|
||||||
|
adapter: AgentFn,
|
||||||
|
extract: LlmExtractorConfig,
|
||||||
|
config: Partial<ReviewerConfig> = {},
|
||||||
|
): Role<ReviewerMeta> {
|
||||||
|
const resolved: ReviewerConfig = { ...defaults, ...config };
|
||||||
|
return createRole(
|
||||||
|
adapter,
|
||||||
|
async (start: StartStep) => reviewerPrompt({ threadId: start.meta.threadId, config: resolved }),
|
||||||
|
reviewerMetaSchema,
|
||||||
|
extract,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"composite": false
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import type {
|
import type { Role, StartStep } from "@uncaged/nerve-core";
|
||||||
Role,
|
|
||||||
RoleResult,
|
|
||||||
StartStep,
|
|
||||||
WorkflowMessage,
|
|
||||||
} from "@uncaged/nerve-core";
|
|
||||||
|
|
||||||
import { START } from "@uncaged/nerve-core";
|
import { START } from "@uncaged/nerve-core";
|
||||||
import { decorateRole, onFail, withDryRun } from "../role-decorators.js";
|
import { decorateRole, onFail, withDryRun } from "../role-decorators.js";
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { isDryRun } from "./role-types.js";
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/** A role decorator: takes a role, returns an enhanced role. */
|
/** A role decorator: takes a role, returns an enhanced role. */
|
||||||
export type RoleDecorator<M extends Record<string, unknown>> = (
|
export type RoleDecorator<M extends Record<string, unknown>> = (role: Role<M>) => Role<M>;
|
||||||
role: Role<M>,
|
|
||||||
) => Role<M>;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// decorateRole — compose a chain of decorators
|
// decorateRole — compose a chain of decorators
|
||||||
@@ -49,16 +47,15 @@ export type WithDryRunOptions<M> = {
|
|||||||
export function withDryRun<M extends Record<string, unknown>>(
|
export function withDryRun<M extends Record<string, unknown>>(
|
||||||
opts: WithDryRunOptions<M>,
|
opts: WithDryRunOptions<M>,
|
||||||
): RoleDecorator<M> {
|
): RoleDecorator<M> {
|
||||||
return (role) =>
|
return (role) => async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||||
async (start: StartStep, messages: WorkflowMessage[]) => {
|
if (isDryRun(start)) {
|
||||||
if (isDryRun(start)) {
|
return {
|
||||||
return {
|
content: `[dry-run] ${opts.label} skipped`,
|
||||||
content: `[dry-run] ${opts.label} skipped`,
|
meta: opts.meta,
|
||||||
meta: opts.meta,
|
};
|
||||||
};
|
}
|
||||||
}
|
return role(start, messages);
|
||||||
return role(start, messages);
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -79,16 +76,15 @@ export type OnFailOptions<M> = {
|
|||||||
export function onFail<M extends Record<string, unknown>>(
|
export function onFail<M extends Record<string, unknown>>(
|
||||||
opts: OnFailOptions<M>,
|
opts: OnFailOptions<M>,
|
||||||
): RoleDecorator<M> {
|
): RoleDecorator<M> {
|
||||||
return (role) =>
|
return (role) => async (start: StartStep, messages: WorkflowMessage[]) => {
|
||||||
async (start: StartStep, messages: WorkflowMessage[]) => {
|
try {
|
||||||
try {
|
return await role(start, messages);
|
||||||
return await role(start, messages);
|
} catch (e) {
|
||||||
} catch (e) {
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
return {
|
||||||
return {
|
content: `${opts.label} failed: ${msg}`,
|
||||||
content: `${opts.label} failed: ${msg}`,
|
meta: opts.meta,
|
||||||
meta: opts.meta,
|
};
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+22
@@ -178,6 +178,28 @@ importers:
|
|||||||
specifier: ^4.1.5
|
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))
|
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/role-reviewer:
|
||||||
|
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/skills: {}
|
||||||
|
|
||||||
packages/store:
|
packages/store:
|
||||||
|
|||||||
Reference in New Issue
Block a user