feat: add reviewer role to all three workflows

- workflow-generator, sense-generator, gitea-issue-solver all now have:
  planner → coder → reviewer → tester → committer → END
- Reviewer uses createHermesRole with git diff/status for static analysis
- Checks: garbage files, secrets, debug code, unrelated changes
- Planner prompt now requires Role Behavior sections for every role
- Coder prompt now emphasizes reading initial user prompt for specifics
This commit is contained in:
小橘 2026-04-28 13:56:37 +00:00
parent bd89dcaff6
commit daf07b5746
14 changed files with 212 additions and 25 deletions

View File

@ -1,18 +1,21 @@
import type { WorkflowDefinition } from "@uncaged/nerve-core";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { moderator } from "./moderator.js";
import type { WorkflowMeta } from "./moderator.js";
import { buildIntakeRole } from "./roles/intake/index.js";
import { buildIssueReaderRole } from "./roles/issue-reader/index.js";
import { buildPlannerRole } from "./roles/planner/index.js";
import { buildImplementerRole } from "./roles/implementer/index.js";
import { buildReviewerRole } from "./roles/reviewer/index.js";
import { buildTesterRole } from "./roles/tester/index.js";
import { buildPrPublisherRole } from "./roles/pr-publisher/index.js";
export type BuildGiteaIssueSolverDeps = {
nerveRoot: string;
provider: LlmProvider | null;
};
export function buildGiteaIssueSolver({ nerveRoot }: BuildGiteaIssueSolverDeps): WorkflowDefinition<WorkflowMeta> {
export function buildGiteaIssueSolver({ nerveRoot, provider }: BuildGiteaIssueSolverDeps): WorkflowDefinition<WorkflowMeta> {
return {
name: "gitea-issue-solver",
roles: {
@ -20,6 +23,7 @@ export function buildGiteaIssueSolver({ nerveRoot }: BuildGiteaIssueSolverDeps):
"issue-reader": buildIssueReaderRole({ nerveRoot }),
planner: buildPlannerRole({ nerveRoot }),
implementer: buildImplementerRole({ nerveRoot }),
reviewer: provider ? buildReviewerRole({ provider, nerveRoot }) : buildReviewerRole({ provider: { apiKey: "", baseUrl: "", model: "" }, nerveRoot }),
tester: buildTesterRole({ nerveRoot }),
"pr-publisher": buildPrPublisherRole({ nerveRoot }),
},

View File

@ -1,9 +1,12 @@
import { join } from "node:path";
import { buildGiteaIssueSolver } from "./build.js";
import { resolveDashScopeProvider } from "./lib/provider.js";
const HOME = process.env.HOME ?? "/home/azureuser";
const NERVE_ROOT = join(HOME, ".uncaged-nerve");
const workflow = buildGiteaIssueSolver({ nerveRoot: NERVE_ROOT });
const provider = await resolveDashScopeProvider(NERVE_ROOT);
const workflow = buildGiteaIssueSolver({ nerveRoot: NERVE_ROOT, provider });
export default workflow;

View File

@ -5,6 +5,7 @@ import type { IntakeMeta } from "./roles/intake/index.js";
import type { IssueReaderMeta } from "./roles/issue-reader/index.js";
import type { PlannerMeta } from "./roles/planner/index.js";
import type { ImplementerMeta } from "./roles/implementer/index.js";
import type { ReviewerMeta } from "./roles/reviewer/index.js";
import type { TesterMeta } from "./roles/tester/index.js";
import type { PrPublisherMeta } from "./roles/pr-publisher/index.js";
@ -13,6 +14,7 @@ export type WorkflowMeta = {
"issue-reader": IssueReaderMeta;
planner: PlannerMeta;
implementer: ImplementerMeta;
reviewer: ReviewerMeta;
tester: TesterMeta;
"pr-publisher": PrPublisherMeta;
};
@ -42,7 +44,13 @@ export const moderator: Moderator<WorkflowMeta> = (context) => {
}
if (last.role === "implementer") {
return "tester";
return "reviewer";
}
if (last.role === "reviewer") {
const meta = last.meta as WorkflowMeta["reviewer"];
if (meta.approved) return "tester";
return "implementer";
}
if (last.role === "tester") {

View File

@ -0,0 +1,21 @@
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
import { reviewerPrompt } from "./prompt.js";
import { z } from "zod";
export const reviewerMetaSchema = z.object({
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
});
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
export type BuildReviewerDeps = {
provider: LlmProvider;
nerveRoot: string;
};
export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) {
return createHermesRole<ReviewerMeta>({
prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }),
extract: { provider, schema: reviewerMetaSchema },
});
}

View File

@ -0,0 +1,46 @@
export function reviewerPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
return `You are a **code reviewer** for Nerve workflow changes. You run after the coder and before the tester.
**IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. Always \`cd ${nerveRoot}\` first.**
Read the workflow thread for context: \`nerve thread ${threadId}\`
## Your job static analysis of the git diff
Run these commands and analyze the output:
1. **\`cd ${nerveRoot} && git diff --stat\`** — see what files changed
2. **\`cd ${nerveRoot} && git diff\`** — read the actual diff
3. **\`cd ${nerveRoot} && git status --short\`** — check for untracked files
## Checklist
### 🔴 Must reject (approved: false)
- **Garbage files**: node_modules/, .DS_Store, pnpm cache artifacts (e.g. \`false/\` directory), build outputs (dist/) that shouldn't be committed
- **Secrets/credentials**: API keys, tokens, passwords hardcoded in the diff
- **Unrelated changes**: files modified outside the scope of the planner's design
### Flag but may still approve
- Debug code left behind (console.log, debugger, TODO/FIXME without context)
- Hardcoded paths that should be configurable
- Missing type exports that other files might need
### Verify
- All files from the planner's design are created/modified
- Code follows existing patterns (compare with sibling workflows)
- Meta types are simple routing signals (single boolean per role)
- No dynamic import()
## Output
Summarize what you found. If garbage files or secrets are present, list them explicitly so the coder knows what to clean up.
End with:
\`\`\`json
{ "approved": true }
\`\`\`
or
\`\`\`json
{ "approved": false }
\`\`\``;
}

View File

@ -2,6 +2,7 @@ import type { WorkflowDefinition } from "@uncaged/nerve-core";
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { buildPlannerRole } from "./roles/planner/index.js";
import { buildCoderRole } from "./roles/coder/index.js";
import { buildReviewerRole } from "./roles/reviewer/index.js";
import { buildTesterRole } from "./roles/tester/index.js";
import { buildCommitterRole } from "./roles/committer/index.js";
import { moderator } from "./moderator.js";
@ -21,6 +22,7 @@ export function buildSenseGenerator({
roles: {
planner: buildPlannerRole({ provider, cwd }),
coder: buildCoderRole({ provider, cwd }),
reviewer: buildReviewerRole({ provider, nerveRoot: cwd }),
tester: buildTesterRole({ provider, nerveRoot: cwd }),
committer: buildCommitterRole({ nerveRoot: cwd }),
},

View File

@ -2,12 +2,14 @@ import { END } from "@uncaged/nerve-core";
import type { Moderator } from "@uncaged/nerve-core";
import type { PlannerMeta } from "./roles/planner/index.js";
import type { CoderMeta } from "./roles/coder/index.js";
import type { ReviewerMeta } from "./roles/reviewer/index.js";
import type { TesterMeta } from "./roles/tester/index.js";
import type { CommitterMeta } from "./roles/committer/index.js";
export type SenseMeta = {
planner: PlannerMeta;
coder: CoderMeta;
reviewer: ReviewerMeta;
tester: TesterMeta;
committer: CommitterMeta;
};
@ -24,7 +26,11 @@ export const moderator: Moderator<SenseMeta> = (context) => {
const coderCount = context.steps.filter((s) => s.role === "coder").length;
if (last.role === "planner") return "coder";
if (last.role === "coder") return "tester";
if (last.role === "coder") return "reviewer";
if (last.role === "reviewer") {
if (last.meta.approved) return "tester";
return coderCount < MAX_CODER_ITERATIONS ? "coder" : END;
}
if (last.role === "tester") {
if (last.meta.passed) return "committer";
const testerCount = countRole(context.steps, "tester");

View File

@ -0,0 +1,21 @@
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
import { reviewerPrompt } from "./prompt.js";
import { z } from "zod";
export const reviewerMetaSchema = z.object({
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
});
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
export type BuildReviewerDeps = {
provider: LlmProvider;
nerveRoot: string;
};
export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) {
return createHermesRole<ReviewerMeta>({
prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }),
extract: { provider, schema: reviewerMetaSchema },
});
}

View File

@ -0,0 +1,46 @@
export function reviewerPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
return `You are a **code reviewer** for Nerve workflow changes. You run after the coder and before the tester.
**IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. Always \`cd ${nerveRoot}\` first.**
Read the workflow thread for context: \`nerve thread ${threadId}\`
## Your job static analysis of the git diff
Run these commands and analyze the output:
1. **\`cd ${nerveRoot} && git diff --stat\`** — see what files changed
2. **\`cd ${nerveRoot} && git diff\`** — read the actual diff
3. **\`cd ${nerveRoot} && git status --short\`** — check for untracked files
## Checklist
### 🔴 Must reject (approved: false)
- **Garbage files**: node_modules/, .DS_Store, pnpm cache artifacts (e.g. \`false/\` directory), build outputs (dist/) that shouldn't be committed
- **Secrets/credentials**: API keys, tokens, passwords hardcoded in the diff
- **Unrelated changes**: files modified outside the scope of the planner's design
### Flag but may still approve
- Debug code left behind (console.log, debugger, TODO/FIXME without context)
- Hardcoded paths that should be configurable
- Missing type exports that other files might need
### Verify
- All files from the planner's design are created/modified
- Code follows existing patterns (compare with sibling workflows)
- Meta types are simple routing signals (single boolean per role)
- No dynamic import()
## Output
Summarize what you found. If garbage files or secrets are present, list them explicitly so the coder knows what to clean up.
End with:
\`\`\`json
{ "approved": true }
\`\`\`
or
\`\`\`json
{ "approved": false }
\`\`\``;
}

View File

@ -23,7 +23,7 @@ export function buildWorkflowGenerator({
roles: {
planner: buildPlannerRole({ provider, cwd: nerveRoot }),
coder: buildCoderRole({ provider, cwd: nerveRoot }),
reviewer: buildReviewerRole({ provider, cwd: nerveRoot }),
reviewer: buildReviewerRole({ provider, nerveRoot }),
tester: buildTesterRole({ provider, nerveRoot }),
committer: buildCommitterRole({ nerveRoot }),
},

View File

@ -7,6 +7,11 @@ Also look at existing workflows in the \`workflows/\` directory for patterns.
Implement the planner's design. This may be **creating a new workflow** or **modifying an existing one**. If there is reviewer, tester, or committer feedback in the thread, fix the issues they identified.
**IMPORTANT:** The thread contains both the **initial user prompt** (the first message) and the **planner's design**. Read both carefully:
- The **initial prompt** contains the user's specific requirements for role behavior, tools to use, and acceptance criteria
- The **planner's design** contains the architecture, file structure, and routing logic
- When writing role prompts, follow the user's behavioral requirements from the initial prompt do not invent your own interpretation
## Multi-step approach
You do NOT need to finish everything in one pass. You may return \`done: false\` to continue in the next iteration. For example:

View File

@ -27,6 +27,12 @@ For **modifications to existing workflows**:
- Impact on moderator routing logic (this workflow's typical order is planner coder reviewer tester committer)
- Backward compatibility considerations (if any)
**For every role (new or modified)**, include a **Role Behavior** section that describes:
- What the role should do, check, or produce
- What tools or commands it should use
- What criteria determine its meta output (e.g. approved/passed/done)
- Preserve the user's specific requirements verbatim do NOT summarize away details
If requirements are NOT clear, describe what is missing or ambiguous.
End your response with a JSON block:

View File

@ -1,23 +1,21 @@
import type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { createCursorRole } from "@uncaged/nerve-workflow-utils";
import { createHermesRole } from "@uncaged/nerve-workflow-utils";
import { reviewerPrompt } from "./prompt.js";
import { z } from "zod";
export const reviewerMetaSchema = z.object({
approved: z.boolean().describe("true if the workflow matches the plan and is ready for tester validation"),
approved: z.boolean().describe("true if the diff is clean and ready for tester validation"),
});
export type ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
export type BuildReviewerDeps = {
provider: LlmProvider;
cwd: string;
nerveRoot: string;
};
export function buildReviewerRole({ provider, cwd }: BuildReviewerDeps) {
return createCursorRole<ReviewerMeta>({
cwd,
mode: "ask",
prompt: async (threadId) => reviewerPrompt({ threadId }),
export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) {
return createHermesRole<ReviewerMeta>({
prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }),
extract: { provider, schema: reviewerMetaSchema },
});
}

View File

@ -1,20 +1,41 @@
export function reviewerPrompt({ threadId }: { threadId: string }): string {
return `You are a **reviewer** for Nerve workflow generation. You run **after** the coder and **before** the tester.
export function reviewerPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
return `You are a **code reviewer** for Nerve workflow changes. You run after the coder and before the tester.
Read the workflow thread for the planner's design and any prior feedback: \`nerve thread ${threadId}\`
Read workflow conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` (from the repo root after \`cd\`).
**IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. Always \`cd ${nerveRoot}\` first.**
## Your job
Read the workflow thread for context: \`nerve thread ${threadId}\`
1. Identify the target workflow name and paths from the thread (e.g. \`workflows/<name>/\`).
2. Read the relevant files: \`moderator.ts\`, \`build.ts\`, \`index.ts\`, and each role under \`roles/\`.
3. Check that the implementation matches the planner's plan (roles, routing, meta types, no forbidden patterns like dynamic \`import()\`).
4. Do **not** run full build/test here that is the tester's job. Flag obvious gaps (missing files, wrong exports, broken routing).
## Your job static analysis of the git diff
If the work is **ready for the tester** to validate builds and config, set \`approved: true\`.
If the coder must fix issues first, set \`approved: false\` and explain briefly what is wrong.
Run these commands and analyze the output:
End with a JSON block:
1. **\`cd ${nerveRoot} && git diff --stat\`** — see what files changed
2. **\`cd ${nerveRoot} && git diff\`** — read the actual diff
3. **\`cd ${nerveRoot} && git status --short\`** — check for untracked files
## Checklist
### 🔴 Must reject (approved: false)
- **Garbage files**: node_modules/, .DS_Store, pnpm cache artifacts (e.g. \`false/\` directory), build outputs (dist/) that shouldn't be committed
- **Secrets/credentials**: API keys, tokens, passwords hardcoded in the diff
- **Unrelated changes**: files modified outside the scope of the planner's design
### Flag but may still approve
- Debug code left behind (console.log, debugger, TODO/FIXME without context)
- Hardcoded paths that should be configurable
- Missing type exports that other files might need
### Verify
- All files from the planner's design are created/modified
- Code follows existing patterns (compare with sibling workflows)
- Meta types are simple routing signals (single boolean per role)
- No dynamic import()
## Output
Summarize what you found. If garbage files or secrets are present, list them explicitly so the coder knows what to clean up.
End with:
\`\`\`json
{ "approved": true }
\`\`\`