feat: @uncaged/workflow-role-committer + @uncaged/workflow-role-reviewer
- Committer: git add/commit/push with LLM-generated branch+message - Reviewer: code review role with approval meta - Both use zod@4 schemas, no nerve-core deps - 98 tests pass, biome clean Closes #12 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
import { describe, expect, spyOn, test } from "bun:test";
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { appendFile, mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
import type { AgentFn, ThreadContext } from "@uncaged/workflow";
|
||||||
|
import { START } from "@uncaged/workflow";
|
||||||
|
import * as workflowRoleLlm from "@uncaged/workflow-role-llm";
|
||||||
|
|
||||||
|
import { createCommitterRole } from "../src/committer.js";
|
||||||
|
import { gitExec } from "../src/git-exec.js";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
async function git(repo: string, args: string[]): Promise<void> {
|
||||||
|
await gitExec(repo, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupRepoWithRemote(): Promise<{ repo: string }> {
|
||||||
|
const base = await mkdtemp(join(tmpdir(), "wf-committer-"));
|
||||||
|
const bare = join(base, "origin.git");
|
||||||
|
const repo = join(base, "work");
|
||||||
|
await mkdir(repo, { recursive: true });
|
||||||
|
await mkdir(bare, { recursive: true });
|
||||||
|
await execFileAsync("git", ["init"], { cwd: repo, encoding: "utf8" });
|
||||||
|
await git(repo, ["config", "user.email", "t@t"]);
|
||||||
|
await git(repo, ["config", "user.name", "t"]);
|
||||||
|
await writeFile(join(repo, "README.md"), "# hi\n", "utf8");
|
||||||
|
await git(repo, ["add", "README.md"]);
|
||||||
|
await git(repo, ["commit", "-m", "init"]);
|
||||||
|
await execFileAsync("git", ["init", "--bare"], { cwd: bare, encoding: "utf8" });
|
||||||
|
await git(repo, ["remote", "add", "origin", bare]);
|
||||||
|
await git(repo, ["push", "-u", "origin", "HEAD"]);
|
||||||
|
return { repo };
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCtx(): ThreadContext {
|
||||||
|
return {
|
||||||
|
start: {
|
||||||
|
role: START,
|
||||||
|
content: "do thing",
|
||||||
|
meta: { maxRounds: 10 },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = { baseUrl: "https://example.com/v1", apiKey: "k", model: "m" };
|
||||||
|
|
||||||
|
describe("createCommitterRole", () => {
|
||||||
|
test("returns committed false when working tree clean", async () => {
|
||||||
|
const { repo } = await setupRepoWithRemote();
|
||||||
|
const agent: AgentFn = async () => {
|
||||||
|
throw new Error("agent should not run");
|
||||||
|
};
|
||||||
|
const role = createCommitterRole(
|
||||||
|
agent,
|
||||||
|
{ provider, dryRun: null },
|
||||||
|
{ cwd: repo, remote: "origin", threadId: null },
|
||||||
|
);
|
||||||
|
const out = await role(makeCtx());
|
||||||
|
expect(out.meta.committed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dry-run skips pipeline", async () => {
|
||||||
|
const agent: AgentFn = async () => {
|
||||||
|
throw new Error("agent should not run");
|
||||||
|
};
|
||||||
|
const role = createCommitterRole(agent, { provider, dryRun: true });
|
||||||
|
const out = await role(makeCtx());
|
||||||
|
expect(out.content).toBe("[dry-run] committer skipped");
|
||||||
|
expect(out.meta).toEqual({ committed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("commits and pushes when changes exist", async () => {
|
||||||
|
const { repo } = await setupRepoWithRemote();
|
||||||
|
await appendFile(join(repo, "README.md"), "\nmore\n", "utf8");
|
||||||
|
|
||||||
|
const spy = spyOn(workflowRoleLlm, "extractMetaOrThrow").mockResolvedValue({
|
||||||
|
branch: "feat/test-commit",
|
||||||
|
message: "feat: add more",
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent: AgentFn = async () => "plan text";
|
||||||
|
const role = createCommitterRole(
|
||||||
|
agent,
|
||||||
|
{ provider, dryRun: null },
|
||||||
|
{ cwd: repo, remote: "origin", threadId: null },
|
||||||
|
);
|
||||||
|
|
||||||
|
const out = await role(makeCtx());
|
||||||
|
expect(out.meta.committed).toBe(true);
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const branches = await gitExec(repo, ["branch", "--list", "feat/test-commit"]);
|
||||||
|
expect(branches).toContain("feat/test-commit");
|
||||||
|
|
||||||
|
const remoteRefs = await gitExec(repo, ["ls-remote", "--heads", "origin", "feat/test-commit"]);
|
||||||
|
expect(remoteRefs.trim().length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/workflow-role-committer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "echo 'TODO'",
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow": "workspace:*",
|
||||||
|
"@uncaged/workflow-role-llm": "workspace:*",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import type { AgentFn, Role, RoleResult, ThreadContext } from "@uncaged/workflow";
|
||||||
|
import {
|
||||||
|
decorateRole,
|
||||||
|
extractMetaOrThrow,
|
||||||
|
type LlmProvider,
|
||||||
|
onFail,
|
||||||
|
withDryRun,
|
||||||
|
} from "@uncaged/workflow-role-llm";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
import { gitExec } from "./git-exec.js";
|
||||||
|
|
||||||
|
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>;
|
||||||
|
|
||||||
|
const committerPlanSchema = z.object({
|
||||||
|
branch: z.string().describe("Feature branch name, e.g. feat/slug or fix/slug"),
|
||||||
|
message: z.string().describe("Single-line conventional commit subject"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CommitterGitConfig = {
|
||||||
|
cwd: string;
|
||||||
|
remote: string;
|
||||||
|
/** When non-null, prompts mention `uncaged-workflow thread <id>` for extra context. */
|
||||||
|
threadId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_COMMITTER_GIT_CONFIG: CommitterGitConfig = {
|
||||||
|
cwd: ".",
|
||||||
|
remote: "origin",
|
||||||
|
threadId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveExtractDryRun(extractDryRun: boolean | null): boolean {
|
||||||
|
return extractDryRun === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeThreadContext(ctx: ThreadContext): string {
|
||||||
|
const lines: string[] = [`Initial prompt:\n${ctx.start.content}`];
|
||||||
|
for (const step of ctx.steps) {
|
||||||
|
const snippet = step.content.length > 800 ? `${step.content.slice(0, 800)}…` : step.content;
|
||||||
|
lines.push(`\n### ${step.role}\n${snippet}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeBranch(branch: string): string {
|
||||||
|
const t = branch.trim();
|
||||||
|
if (
|
||||||
|
t === "" ||
|
||||||
|
t.includes("..") ||
|
||||||
|
t.includes(" ") ||
|
||||||
|
t.startsWith("-") ||
|
||||||
|
t.includes("\n") ||
|
||||||
|
t.includes("\t")
|
||||||
|
) {
|
||||||
|
throw new Error(`invalid branch name: ${branch}`);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeCommitMessage(message: string): string {
|
||||||
|
const line = message.trim().split(/\r?\n/)[0] ?? "";
|
||||||
|
if (line === "") {
|
||||||
|
throw new Error("commit message is empty");
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
function committerPlanPrompt(ctx: ThreadContext, gitConfig: CommitterGitConfig): string {
|
||||||
|
const threadLine =
|
||||||
|
gitConfig.threadId !== null
|
||||||
|
? `Optional CLI context: run \`uncaged-workflow thread ${gitConfig.threadId}\` if available.\n`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `You plan a git branch and a single-line conventional commit message for the following workflow thread.
|
||||||
|
|
||||||
|
${threadLine}
|
||||||
|
## Thread context
|
||||||
|
|
||||||
|
${summarizeThreadContext(ctx)}
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Infer a good branch name (\`feat/<slug>\` or \`fix/<slug>\`) and a conventional commit **subject** (one line, no body).
|
||||||
|
|
||||||
|
Reply with enough detail that a maintainer understands the change; structured extraction will read \`branch\` and \`message\` from your answer.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommitterPipeline(
|
||||||
|
ctx: ThreadContext,
|
||||||
|
agent: AgentFn,
|
||||||
|
extract: { provider: LlmProvider; dryRun: boolean | null },
|
||||||
|
gitConfig: CommitterGitConfig,
|
||||||
|
): Promise<RoleResult<CommitterMeta>> {
|
||||||
|
const cwd = gitConfig.cwd;
|
||||||
|
const porcelain = await gitExec(cwd, ["status", "--porcelain"]);
|
||||||
|
if (porcelain.trim() === "") {
|
||||||
|
return {
|
||||||
|
content: "Working tree clean; nothing to commit.",
|
||||||
|
meta: { committed: false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = committerPlanPrompt(ctx, gitConfig);
|
||||||
|
const raw = await agent(ctx, prompt);
|
||||||
|
const plan = await extractMetaOrThrow("committer-plan", raw, committerPlanSchema, {
|
||||||
|
provider: extract.provider,
|
||||||
|
dryRun: resolveExtractDryRun(extract.dryRun),
|
||||||
|
});
|
||||||
|
|
||||||
|
const branch = sanitizeBranch(plan.branch);
|
||||||
|
const message = sanitizeCommitMessage(plan.message);
|
||||||
|
|
||||||
|
await gitExec(cwd, ["checkout", "-b", branch]);
|
||||||
|
await gitExec(cwd, ["add", "-A"]);
|
||||||
|
await gitExec(cwd, ["commit", "-m", message]);
|
||||||
|
await gitExec(cwd, ["push", "-u", gitConfig.remote, branch]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: raw,
|
||||||
|
meta: { committed: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git committer role: LLM proposes branch + message; this package runs git via `child_process`.
|
||||||
|
* Decorators match nerve semantics: dry-run skips work with `committed: true`; failures yield `committed: false`.
|
||||||
|
*/
|
||||||
|
export function createCommitterRole(
|
||||||
|
adapter: AgentFn,
|
||||||
|
extract: { provider: LlmProvider; dryRun: boolean | null },
|
||||||
|
gitConfig: CommitterGitConfig = DEFAULT_COMMITTER_GIT_CONFIG,
|
||||||
|
): Role<CommitterMeta> {
|
||||||
|
const inner: Role<CommitterMeta> = async (ctx) =>
|
||||||
|
runCommitterPipeline(ctx, adapter, extract, gitConfig);
|
||||||
|
|
||||||
|
return decorateRole(inner, [
|
||||||
|
withDryRun<CommitterMeta>({
|
||||||
|
label: "committer",
|
||||||
|
meta: { committed: true },
|
||||||
|
dryRun: resolveExtractDryRun(extract.dryRun),
|
||||||
|
}),
|
||||||
|
onFail<CommitterMeta>({ label: "committer", meta: { committed: false } }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
/** Runs `git` with args in `cwd`; throws if git exits non-zero. */
|
||||||
|
export async function gitExec(cwd: string, args: readonly string[]): Promise<string> {
|
||||||
|
try {
|
||||||
|
const r = await execFileAsync("git", [...args], {
|
||||||
|
cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
return r.stdout;
|
||||||
|
} catch (e) {
|
||||||
|
const stderr =
|
||||||
|
typeof e === "object" &&
|
||||||
|
e !== null &&
|
||||||
|
"stderr" in e &&
|
||||||
|
typeof (e as { stderr: unknown }).stderr === "string"
|
||||||
|
? (e as { stderr: string }).stderr
|
||||||
|
: "";
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
throw new Error(`git ${args.join(" ")} failed: ${msg}${stderr ? ` (${stderr.trim()})` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export {
|
||||||
|
type CommitterGitConfig,
|
||||||
|
type CommitterMeta,
|
||||||
|
committerMetaSchema,
|
||||||
|
createCommitterRole,
|
||||||
|
DEFAULT_COMMITTER_GIT_CONFIG,
|
||||||
|
} from "./committer.js";
|
||||||
|
export { gitExec } from "./git-exec.js";
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [{ "path": "../workflow" }, { "path": "../workflow-role-llm" }]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,88 @@
|
|||||||
|
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||||
|
|
||||||
|
import type { AgentFn, ThreadContext } from "@uncaged/workflow";
|
||||||
|
import { START } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
import { createReviewerRole, DEFAULT_REVIEWER_CONFIG } from "../src/reviewer.js";
|
||||||
|
|
||||||
|
function toolCallResponse(argsJson: string): Response {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
function: {
|
||||||
|
name: "extract",
|
||||||
|
arguments: argsJson,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCtx(): ThreadContext {
|
||||||
|
return {
|
||||||
|
start: {
|
||||||
|
role: START,
|
||||||
|
content: "task",
|
||||||
|
meta: { maxRounds: 10 },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = { baseUrl: "https://example.com/v1", apiKey: "k", model: "m" };
|
||||||
|
|
||||||
|
describe("createReviewerRole", () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runs reviewer extract", async () => {
|
||||||
|
globalThis.fetch = () => Promise.resolve(toolCallResponse(JSON.stringify({ approved: true })));
|
||||||
|
|
||||||
|
const agent: AgentFn = async (_ctx, prompt) => {
|
||||||
|
expect(prompt).toContain("git diff");
|
||||||
|
expect(prompt).toContain(DEFAULT_REVIEWER_CONFIG.cwd);
|
||||||
|
return "review done";
|
||||||
|
};
|
||||||
|
|
||||||
|
const role = createReviewerRole(agent, { provider, dryRun: null });
|
||||||
|
const out = await role(makeCtx());
|
||||||
|
expect(out.meta).toEqual({ approved: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("includes uncaged-workflow thread hint when threadId set", async () => {
|
||||||
|
globalThis.fetch = () => Promise.resolve(toolCallResponse(JSON.stringify({ approved: false })));
|
||||||
|
|
||||||
|
let seen = "";
|
||||||
|
const agent: AgentFn = async (_ctx, prompt) => {
|
||||||
|
seen = prompt;
|
||||||
|
return "x";
|
||||||
|
};
|
||||||
|
|
||||||
|
const role = createReviewerRole(
|
||||||
|
agent,
|
||||||
|
{ provider, dryRun: null },
|
||||||
|
{
|
||||||
|
cwd: "/proj",
|
||||||
|
conventionsPath: null,
|
||||||
|
extraChecks: [],
|
||||||
|
threadId: "01ABCDEF234567890ABCDEFGH",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await role(makeCtx());
|
||||||
|
expect(seen).toContain("uncaged-workflow thread 01ABCDEF234567890ABCDEFGH");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "@uncaged/workflow-role-reviewer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "echo 'TODO'",
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/workflow": "workspace:*",
|
||||||
|
"@uncaged/workflow-role-llm": "workspace:*",
|
||||||
|
"zod": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
createReviewerRole,
|
||||||
|
DEFAULT_REVIEWER_CONFIG,
|
||||||
|
type ReviewerConfig,
|
||||||
|
type ReviewerMeta,
|
||||||
|
reviewerMetaSchema,
|
||||||
|
} from "./reviewer.js";
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import type { AgentFn, Role, ThreadContext } from "@uncaged/workflow";
|
||||||
|
import { createRole, type LlmProvider } from "@uncaged/workflow-role-llm";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
cwd: string;
|
||||||
|
conventionsPath: string | null;
|
||||||
|
extraChecks: ReadonlyArray<string>;
|
||||||
|
/** When non-null, prompts reference `uncaged-workflow thread <id>`. */
|
||||||
|
threadId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
|
||||||
|
cwd: ".",
|
||||||
|
conventionsPath: "CONVENTIONS.md",
|
||||||
|
extraChecks: [],
|
||||||
|
threadId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function summarizeThreadContext(ctx: ThreadContext): string {
|
||||||
|
const lines: string[] = [`Initial prompt:\n${ctx.start.content}`];
|
||||||
|
for (const step of ctx.steps) {
|
||||||
|
const snippet = step.content.length > 600 ? `${step.content.slice(0, 600)}…` : step.content;
|
||||||
|
lines.push(`\n### ${step.role}\n${snippet}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviewerPrompt(config: ReviewerConfig, ctx: ThreadContext): string {
|
||||||
|
const { cwd, conventionsPath, extraChecks, threadId } = config;
|
||||||
|
|
||||||
|
const conventionsBlock =
|
||||||
|
conventionsPath !== null ? `Read project conventions: \`cat ${cwd}/${conventionsPath}\`\n` : "";
|
||||||
|
|
||||||
|
const threadBlock =
|
||||||
|
threadId !== null
|
||||||
|
? `Read the workflow thread for context: \`uncaged-workflow thread ${threadId}\`\n`
|
||||||
|
: `## Thread context (no thread id)\n\n${summarizeThreadContext(ctx)}\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.**
|
||||||
|
|
||||||
|
${threadBlock}
|
||||||
|
${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 should not be committed
|
||||||
|
- **Secrets/credentials**: API keys, tokens, passwords hardcoded in the diff
|
||||||
|
- **Unrelated changes**: files modified outside the scope of the task
|
||||||
|
${
|
||||||
|
conventionsPath !== null
|
||||||
|
? `- **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 }
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code review role: agent inspects git diffs; structured extract yields `approved`.
|
||||||
|
*/
|
||||||
|
export function createReviewerRole(
|
||||||
|
adapter: AgentFn,
|
||||||
|
extract: { provider: LlmProvider; dryRun: boolean | null },
|
||||||
|
config: ReviewerConfig = DEFAULT_REVIEWER_CONFIG,
|
||||||
|
): Role<ReviewerMeta> {
|
||||||
|
return createRole({
|
||||||
|
name: "reviewer",
|
||||||
|
schema: reviewerMetaSchema,
|
||||||
|
systemPrompt: async (ctx) => reviewerPrompt(config, ctx),
|
||||||
|
agent: adapter,
|
||||||
|
extract: { provider: extract.provider, dryRun: extract.dryRun },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"references": [{ "path": "../workflow" }, { "path": "../workflow-role-llm" }]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -18,6 +18,8 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "packages/workflow" },
|
{ "path": "packages/workflow" },
|
||||||
{ "path": "packages/workflow-role-llm" },
|
{ "path": "packages/workflow-role-llm" },
|
||||||
|
{ "path": "packages/workflow-role-committer" },
|
||||||
|
{ "path": "packages/workflow-role-reviewer" },
|
||||||
{ "path": "packages/workflow-agent-cursor" },
|
{ "path": "packages/workflow-agent-cursor" },
|
||||||
{ "path": "packages/workflow-agent-hermes" },
|
{ "path": "packages/workflow-agent-hermes" },
|
||||||
{ "path": "packages/cli-workflow" }
|
{ "path": "packages/cli-workflow" }
|
||||||
|
|||||||
Reference in New Issue
Block a user