This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@uncaged/workflow-role-preparer",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'TODO'",
|
||||
"test": "echo no tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/workflow": "workspace:*",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
type PreparerMeta,
|
||||
preparerMetaSchema,
|
||||
preparerRole,
|
||||
} from "./preparer.js";
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
const toolchainSchema = z.object({
|
||||
packageManager: z.union([z.string(), z.null()]),
|
||||
testCommand: z.union([z.string(), z.null()]),
|
||||
lintCommand: z.union([z.string(), z.null()]),
|
||||
buildCommand: z.union([z.string(), z.null()]),
|
||||
});
|
||||
|
||||
export const preparerMetaSchema = z.object({
|
||||
repoPath: z.string(),
|
||||
defaultBranch: z.string(),
|
||||
conventions: z.union([z.string(), z.null()]),
|
||||
toolchain: toolchainSchema,
|
||||
});
|
||||
|
||||
export type PreparerMeta = z.infer<typeof preparerMetaSchema>;
|
||||
|
||||
const PREPARER_SYSTEM = `You are a **preparer** for a software task. Your job is to locate (or clone) the target repository locally, ensure it is up to date, and gather project context before work begins.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. Parse the issue/task prompt to identify the target repository (URL, org/repo, or name).
|
||||
2. Search for an existing local clone in these locations (in order):
|
||||
- ~/Code/<repo-name>/
|
||||
- ~/repos/<repo-name>/
|
||||
- ~/Code/<org>/<repo-name>/
|
||||
- ~/repos/<org>/<repo-name>/
|
||||
3. If not found locally, \`git clone\` it into ~/repos/<repo-name>/.
|
||||
4. \`git checkout main && git pull\` (or the default branch) to ensure latest.
|
||||
5. Read project conventions: \`CLAUDE.md\`, \`CONTRIBUTING.md\`, \`.cursor/rules/*.mdc\`, \`CONVENTIONS.md\`.
|
||||
6. Detect toolchain: package manager, test runner, linter, build system.
|
||||
|
||||
## Output
|
||||
|
||||
Report your findings as structured data:
|
||||
- **repoPath**: absolute path to the local repo
|
||||
- **defaultBranch**: the default branch name (e.g. "main")
|
||||
- **conventions**: a summary of project conventions found, or null if none
|
||||
- **toolchain**: detected commands for packageManager, testCommand, lintCommand, buildCommand (null if not detected)`;
|
||||
|
||||
export const preparerRole: RoleDefinition<PreparerMeta> = {
|
||||
description:
|
||||
"Locates or clones the target repository, ensures it is up to date, and gathers project context (conventions, toolchain).",
|
||||
systemPrompt: PREPARER_SYSTEM,
|
||||
extractPrompt:
|
||||
"Extract repoPath (absolute path), defaultBranch, conventions (summary string or null), and toolchain (packageManager, testCommand, lintCommand, buildCommand — each string or null).",
|
||||
schema: preparerMetaSchema,
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import type { CoderMeta } from "@uncaged/workflow-role-coder";
|
||||
import type { PlannerMeta } from "@uncaged/workflow-role-planner";
|
||||
import type { PreparerMeta } from "@uncaged/workflow-role-preparer";
|
||||
|
||||
import { buildSolveIssueDescriptor } from "../src/descriptor.js";
|
||||
import { createSolveIssueRun, solveIssueModerator } from "../src/index.js";
|
||||
@@ -108,6 +109,25 @@ function makeCtx(
|
||||
};
|
||||
}
|
||||
|
||||
function preparerStep(): RoleStep<SolveIssueMeta> {
|
||||
return {
|
||||
role: "preparer",
|
||||
content: "prepared",
|
||||
meta: {
|
||||
repoPath: "/home/user/repos/test",
|
||||
defaultBranch: "main",
|
||||
conventions: null,
|
||||
toolchain: {
|
||||
packageManager: "bun",
|
||||
testCommand: "bun test",
|
||||
lintCommand: null,
|
||||
buildCommand: "bun run build",
|
||||
},
|
||||
},
|
||||
timestamp: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function plannerStep(phases: PlannerMeta["phases"] = DEFAULT_PHASES): RoleStep<SolveIssueMeta> {
|
||||
return {
|
||||
role: "planner",
|
||||
@@ -153,16 +173,27 @@ const stubExtract = createExtract({
|
||||
});
|
||||
|
||||
describe("solveIssueModerator", () => {
|
||||
test("routes planner → coder → reviewer → committer → END", () => {
|
||||
expect(solveIssueModerator(makeCtx(20, []))).toBe("planner");
|
||||
expect(solveIssueModerator(makeCtx(20, [plannerStep()]))).toBe("coder");
|
||||
expect(solveIssueModerator(makeCtx(20, [plannerStep(), coderStep()]))).toBe("reviewer");
|
||||
expect(solveIssueModerator(makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
|
||||
"committer",
|
||||
test("routes preparer → planner → coder → reviewer → committer → END", () => {
|
||||
expect(solveIssueModerator(makeCtx(20, []))).toBe("preparer");
|
||||
expect(solveIssueModerator(makeCtx(20, [preparerStep()]))).toBe("planner");
|
||||
expect(solveIssueModerator(makeCtx(20, [preparerStep(), plannerStep()]))).toBe("coder");
|
||||
expect(solveIssueModerator(makeCtx(20, [preparerStep(), plannerStep(), coderStep()]))).toBe(
|
||||
"reviewer",
|
||||
);
|
||||
expect(
|
||||
solveIssueModerator(
|
||||
makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true), committerStep()]),
|
||||
makeCtx(20, [preparerStep(), plannerStep(), coderStep(), reviewerStep(true)]),
|
||||
),
|
||||
).toBe("committer");
|
||||
expect(
|
||||
solveIssueModerator(
|
||||
makeCtx(20, [
|
||||
preparerStep(),
|
||||
plannerStep(),
|
||||
coderStep(),
|
||||
reviewerStep(true),
|
||||
committerStep(),
|
||||
]),
|
||||
),
|
||||
).toBe(END);
|
||||
});
|
||||
@@ -250,8 +281,19 @@ describe("createSolveIssueRun", () => {
|
||||
restoreFetch = null;
|
||||
});
|
||||
|
||||
test("structured extraction yields planner meta from mocked chat completions", async () => {
|
||||
restoreFetch = installMockChatCompletions([EXPECT_PLANNER_META]);
|
||||
test("structured extraction yields preparer then planner meta from mocked chat completions", async () => {
|
||||
const EXPECT_PREPARER_META: PreparerMeta = {
|
||||
repoPath: "/home/user/repos/test",
|
||||
defaultBranch: "main",
|
||||
conventions: null,
|
||||
toolchain: {
|
||||
packageManager: "bun",
|
||||
testCommand: "bun test",
|
||||
lintCommand: null,
|
||||
buildCommand: "bun run build",
|
||||
},
|
||||
};
|
||||
restoreFetch = installMockChatCompletions([EXPECT_PREPARER_META, EXPECT_PLANNER_META]);
|
||||
|
||||
const run = createSolveIssueRun({ agent: async () => "" }, stubExtract);
|
||||
const gen = run(
|
||||
@@ -263,12 +305,30 @@ describe("createSolveIssueRun", () => {
|
||||
if (first.done) {
|
||||
throw new Error("expected yield");
|
||||
}
|
||||
expect(first.value.role).toBe("planner");
|
||||
expect(first.value.meta).toEqual(EXPECT_PLANNER_META);
|
||||
expect(first.value.role).toBe("preparer");
|
||||
expect(first.value.meta).toEqual(EXPECT_PREPARER_META);
|
||||
|
||||
const second = await gen.next();
|
||||
expect(second.done).toBe(false);
|
||||
if (second.done) {
|
||||
throw new Error("expected yield");
|
||||
}
|
||||
expect(second.value.role).toBe("planner");
|
||||
expect(second.value.meta).toEqual(EXPECT_PLANNER_META);
|
||||
});
|
||||
|
||||
test("per-role agent overrides default", async () => {
|
||||
restoreFetch = installMockChatCompletions([EXPECT_PLANNER_META, EXPECT_CODER_META]);
|
||||
const PREPARER_META: PreparerMeta = {
|
||||
repoPath: "/tmp/r",
|
||||
defaultBranch: "main",
|
||||
conventions: null,
|
||||
toolchain: { packageManager: null, testCommand: null, lintCommand: null, buildCommand: null },
|
||||
};
|
||||
restoreFetch = installMockChatCompletions([
|
||||
PREPARER_META,
|
||||
EXPECT_PLANNER_META,
|
||||
EXPECT_CODER_META,
|
||||
]);
|
||||
|
||||
const calls: string[] = [];
|
||||
const run = createSolveIssueRun(
|
||||
@@ -278,6 +338,10 @@ describe("createSolveIssueRun", () => {
|
||||
return "";
|
||||
},
|
||||
overrides: {
|
||||
preparer: async () => {
|
||||
calls.push("preparer");
|
||||
return "";
|
||||
},
|
||||
planner: async () => {
|
||||
calls.push("planner");
|
||||
return "";
|
||||
@@ -295,6 +359,10 @@ describe("createSolveIssueRun", () => {
|
||||
{ threadId: "01TEST000000000000000000TR", maxRounds: 20 },
|
||||
);
|
||||
await gen.next();
|
||||
expect(calls).toEqual(["preparer"]);
|
||||
|
||||
calls.length = 0;
|
||||
await gen.next();
|
||||
expect(calls).toEqual(["planner"]);
|
||||
|
||||
calls.length = 0;
|
||||
@@ -315,9 +383,10 @@ describe("buildSolveIssueDescriptor", () => {
|
||||
"coder",
|
||||
"committer",
|
||||
"planner",
|
||||
"preparer",
|
||||
"reviewer",
|
||||
]);
|
||||
for (const key of ["planner", "coder", "reviewer", "committer"] as const) {
|
||||
for (const key of ["preparer", "planner", "coder", "reviewer", "committer"] as const) {
|
||||
const role = validated.value.roles[key];
|
||||
expect(role).toBeDefined();
|
||||
expect(typeof role.schema).toBe("object");
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@uncaged/workflow-role-committer": "workspace:*",
|
||||
"@uncaged/workflow-role-coder": "workspace:*",
|
||||
"@uncaged/workflow-role-planner": "workspace:*",
|
||||
"@uncaged/workflow-role-preparer": "workspace:*",
|
||||
"@uncaged/workflow-role-reviewer": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ export {
|
||||
plannerMetaSchema,
|
||||
plannerRole,
|
||||
} from "@uncaged/workflow-role-planner";
|
||||
export {
|
||||
type PreparerMeta,
|
||||
preparerMetaSchema,
|
||||
preparerRole,
|
||||
} from "@uncaged/workflow-role-preparer";
|
||||
export {
|
||||
type ReviewerMeta,
|
||||
reviewerMetaSchema,
|
||||
|
||||
@@ -48,11 +48,15 @@ export const solveIssueModerator: Moderator<SolveIssueMeta> = (ctx) => {
|
||||
const maxRounds = ctx.start.meta.maxRounds;
|
||||
|
||||
if (ctx.steps.length === 0) {
|
||||
return "planner";
|
||||
return "preparer";
|
||||
}
|
||||
|
||||
const last = ctx.steps[ctx.steps.length - 1];
|
||||
|
||||
if (last.role === "preparer") {
|
||||
return "planner";
|
||||
}
|
||||
|
||||
if (last.role === "planner") {
|
||||
return "coder";
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import { type CoderMeta, coderRole } from "@uncaged/workflow-role-coder";
|
||||
import { type CommitterMeta, committerRole } from "@uncaged/workflow-role-committer";
|
||||
import { type PlannerMeta, plannerRole } from "@uncaged/workflow-role-planner";
|
||||
import { type PreparerMeta, preparerRole } from "@uncaged/workflow-role-preparer";
|
||||
import { type ReviewerMeta, reviewerRole } from "@uncaged/workflow-role-reviewer";
|
||||
|
||||
export const SOLVE_ISSUE_WORKFLOW_DESCRIPTION =
|
||||
"Phased plan, incremental implementation per phase, review, and commit to resolve an issue end-to-end (planner → coder [repeat per phase] → reviewer → committer).";
|
||||
"Prepare repo context, plan phases, implement incrementally, review, and commit to resolve an issue end-to-end (preparer → planner → coder [repeat per phase] → reviewer → committer).";
|
||||
|
||||
export type SolveIssueMeta = {
|
||||
preparer: PreparerMeta;
|
||||
planner: PlannerMeta;
|
||||
coder: CoderMeta;
|
||||
reviewer: ReviewerMeta;
|
||||
@@ -19,6 +21,7 @@ export type SolveIssueRoles = {
|
||||
};
|
||||
|
||||
export const solveIssueRoles: SolveIssueRoles = {
|
||||
preparer: preparerRole,
|
||||
planner: plannerRole,
|
||||
coder: coderRole,
|
||||
reviewer: reviewerRole,
|
||||
|
||||
Reference in New Issue
Block a user