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

View File

@ -1,9 +1,12 @@
import { join } from "node:path"; import { join } from "node:path";
import { buildGiteaIssueSolver } from "./build.js"; import { buildGiteaIssueSolver } from "./build.js";
import { resolveDashScopeProvider } from "./lib/provider.js";
const HOME = process.env.HOME ?? "/home/azureuser"; const HOME = process.env.HOME ?? "/home/azureuser";
const NERVE_ROOT = join(HOME, ".uncaged-nerve"); 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; 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 { IssueReaderMeta } from "./roles/issue-reader/index.js";
import type { PlannerMeta } from "./roles/planner/index.js"; import type { PlannerMeta } from "./roles/planner/index.js";
import type { ImplementerMeta } from "./roles/implementer/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 { TesterMeta } from "./roles/tester/index.js";
import type { PrPublisherMeta } from "./roles/pr-publisher/index.js"; import type { PrPublisherMeta } from "./roles/pr-publisher/index.js";
@ -13,6 +14,7 @@ export type WorkflowMeta = {
"issue-reader": IssueReaderMeta; "issue-reader": IssueReaderMeta;
planner: PlannerMeta; planner: PlannerMeta;
implementer: ImplementerMeta; implementer: ImplementerMeta;
reviewer: ReviewerMeta;
tester: TesterMeta; tester: TesterMeta;
"pr-publisher": PrPublisherMeta; "pr-publisher": PrPublisherMeta;
}; };
@ -42,7 +44,13 @@ export const moderator: Moderator<WorkflowMeta> = (context) => {
} }
if (last.role === "implementer") { 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") { 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 type { LlmProvider } from "@uncaged/nerve-workflow-utils";
import { buildPlannerRole } from "./roles/planner/index.js"; import { buildPlannerRole } from "./roles/planner/index.js";
import { buildCoderRole } from "./roles/coder/index.js"; import { buildCoderRole } from "./roles/coder/index.js";
import { buildReviewerRole } from "./roles/reviewer/index.js";
import { buildTesterRole } from "./roles/tester/index.js"; import { buildTesterRole } from "./roles/tester/index.js";
import { buildCommitterRole } from "./roles/committer/index.js"; import { buildCommitterRole } from "./roles/committer/index.js";
import { moderator } from "./moderator.js"; import { moderator } from "./moderator.js";
@ -21,6 +22,7 @@ export function buildSenseGenerator({
roles: { roles: {
planner: buildPlannerRole({ provider, cwd }), planner: buildPlannerRole({ provider, cwd }),
coder: buildCoderRole({ provider, cwd }), coder: buildCoderRole({ provider, cwd }),
reviewer: buildReviewerRole({ provider, nerveRoot: cwd }),
tester: buildTesterRole({ provider, nerveRoot: cwd }), tester: buildTesterRole({ provider, nerveRoot: cwd }),
committer: buildCommitterRole({ 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 { Moderator } from "@uncaged/nerve-core";
import type { PlannerMeta } from "./roles/planner/index.js"; import type { PlannerMeta } from "./roles/planner/index.js";
import type { CoderMeta } from "./roles/coder/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 { TesterMeta } from "./roles/tester/index.js";
import type { CommitterMeta } from "./roles/committer/index.js"; import type { CommitterMeta } from "./roles/committer/index.js";
export type SenseMeta = { export type SenseMeta = {
planner: PlannerMeta; planner: PlannerMeta;
coder: CoderMeta; coder: CoderMeta;
reviewer: ReviewerMeta;
tester: TesterMeta; tester: TesterMeta;
committer: CommitterMeta; committer: CommitterMeta;
}; };
@ -24,7 +26,11 @@ export const moderator: Moderator<SenseMeta> = (context) => {
const coderCount = context.steps.filter((s) => s.role === "coder").length; const coderCount = context.steps.filter((s) => s.role === "coder").length;
if (last.role === "planner") return "coder"; 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.role === "tester") {
if (last.meta.passed) return "committer"; if (last.meta.passed) return "committer";
const testerCount = countRole(context.steps, "tester"); 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: { roles: {
planner: buildPlannerRole({ provider, cwd: nerveRoot }), planner: buildPlannerRole({ provider, cwd: nerveRoot }),
coder: buildCoderRole({ provider, cwd: nerveRoot }), coder: buildCoderRole({ provider, cwd: nerveRoot }),
reviewer: buildReviewerRole({ provider, cwd: nerveRoot }), reviewer: buildReviewerRole({ provider, nerveRoot }),
tester: buildTesterRole({ provider, nerveRoot }), tester: buildTesterRole({ provider, nerveRoot }),
committer: buildCommitterRole({ 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. 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 ## 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: 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) - Impact on moderator routing logic (this workflow's typical order is planner coder reviewer tester committer)
- Backward compatibility considerations (if any) - 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. If requirements are NOT clear, describe what is missing or ambiguous.
End your response with a JSON block: End your response with a JSON block:

View File

@ -1,23 +1,21 @@
import type { LlmProvider } from "@uncaged/nerve-workflow-utils"; 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 { reviewerPrompt } from "./prompt.js";
import { z } from "zod"; import { z } from "zod";
export const reviewerMetaSchema = z.object({ 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 ReviewerMeta = z.infer<typeof reviewerMetaSchema>;
export type BuildReviewerDeps = { export type BuildReviewerDeps = {
provider: LlmProvider; provider: LlmProvider;
cwd: string; nerveRoot: string;
}; };
export function buildReviewerRole({ provider, cwd }: BuildReviewerDeps) { export function buildReviewerRole({ provider, nerveRoot }: BuildReviewerDeps) {
return createCursorRole<ReviewerMeta>({ return createHermesRole<ReviewerMeta>({
cwd, prompt: async (threadId) => reviewerPrompt({ threadId, nerveRoot }),
mode: "ask",
prompt: async (threadId) => reviewerPrompt({ threadId }),
extract: { provider, schema: reviewerMetaSchema }, extract: { provider, schema: reviewerMetaSchema },
}); });
} }

View File

@ -1,20 +1,41 @@
export function reviewerPrompt({ threadId }: { threadId: string }): string { export function reviewerPrompt({ threadId, nerveRoot }: { threadId: string; nerveRoot: string }): string {
return `You are a **reviewer** for Nerve workflow generation. You run **after** the coder and **before** the tester. 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}\` **IMPORTANT: The Nerve workspace is at \`${nerveRoot}\`. Always \`cd ${nerveRoot}\` first.**
Read workflow conventions: \`cat node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\` (from the repo root after \`cd\`).
## 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>/\`). ## Your job static analysis of the git diff
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).
If the work is **ready for the tester** to validate builds and config, set \`approved: true\`. Run these commands and analyze the output:
If the coder must fix issues first, set \`approved: false\` and explain briefly what is wrong.
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 \`\`\`json
{ "approved": true } { "approved": true }
\`\`\` \`\`\`