feat(planner): add hash and title fields to phase schema
Each phase now carries a hash (Crockford Base32 identifier) and a one-line title alongside the existing name/description/acceptance. This gives agents immediate semantic context in the prompt without needing to load full phase details from CAS. Refs #23
This commit is contained in:
@@ -10,7 +10,7 @@ export const coderMetaSchema = z.object({
|
||||
export type CoderMeta = z.infer<typeof coderMetaSchema>;
|
||||
|
||||
const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only.
|
||||
Report which phase you completed using the planner's exact phase name. If you legitimately finish every remaining phase in this single turn, set completedPhase to the last phase name in the plan (the workflow treats that as full completion). List the files you changed and summarize what you did.`;
|
||||
Each planner phase is identified by a hash (8-char Crockford Base32) and a title (one-line summary). Report which phase you completed using the planner's exact phase name. If you legitimately finish every remaining phase in this single turn, set completedPhase to the last phase name in the plan (the workflow treats that as full completion). List the files you changed and summarize what you did.`;
|
||||
|
||||
export const coderRole: RoleDefinition<CoderMeta> = {
|
||||
description:
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { RoleDefinition } from "@uncaged/workflow";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const phaseSchema = z.object({
|
||||
hash: z.string(),
|
||||
title: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
acceptance: z.string(),
|
||||
@@ -15,7 +17,7 @@ export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
|
||||
|
||||
const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time.
|
||||
|
||||
Each phase must have: a short **name** (stable identifier), a **description** of what to do in that phase, and **acceptance** criteria for when that phase is done.
|
||||
Each phase must have: a short **name** (stable identifier), a one-line **title** summarising the phase goal, a **hash** (8-char Crockford Base32 identifier unique within this plan), a **description** of what to do in that phase, and **acceptance** criteria for when that phase is done.
|
||||
|
||||
Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases. Do not emit separate file lists or a free-form "approach" field — put that detail inside phase descriptions.`;
|
||||
|
||||
@@ -23,6 +25,6 @@ export const plannerRole: RoleDefinition<PlannerMeta> = {
|
||||
description: "Breaks the task into sequential phases for the coder.",
|
||||
systemPrompt: PLANNER_SYSTEM,
|
||||
extractPrompt:
|
||||
"Extract the implementation phases from the agent's analysis. Each phase needs a name, description, and acceptance criteria.",
|
||||
"Extract the implementation phases from the agent's analysis. Each phase needs a hash (8-char Crockford Base32 unique within this plan), a title (one-line summary), a name, a description, and acceptance criteria.",
|
||||
schema: plannerMetaSchema,
|
||||
};
|
||||
|
||||
@@ -16,11 +16,11 @@ import { createSolveIssueRun, solveIssueModerator } from "../src/index.js";
|
||||
import type { SolveIssueMeta } from "../src/roles.js";
|
||||
|
||||
const DEFAULT_PHASES: PlannerMeta["phases"] = [
|
||||
{ name: "phase-a", description: "Do the work", acceptance: "Done" },
|
||||
{ hash: "4KNMR2PX", title: "Do the work", name: "phase-a", description: "Do the work", acceptance: "Done" },
|
||||
];
|
||||
|
||||
const EXPECT_PLANNER_META: PlannerMeta = {
|
||||
phases: [{ name: "phase-1", description: "placeholder", acceptance: "placeholder" }],
|
||||
phases: [{ hash: "7BQST3VW", title: "placeholder phase", name: "phase-1", description: "placeholder", acceptance: "placeholder" }],
|
||||
};
|
||||
|
||||
const EXPECT_CODER_META: CoderMeta = {
|
||||
@@ -179,8 +179,8 @@ describe("solveIssueModerator", () => {
|
||||
|
||||
test("multiple planner phases → coder until all complete, then reviewer", () => {
|
||||
const phases: PlannerMeta["phases"] = [
|
||||
{ name: "p1", description: "first", acceptance: "a1" },
|
||||
{ name: "p2", description: "second", acceptance: "a2" },
|
||||
{ hash: "AA000001", title: "first phase", name: "p1", description: "first", acceptance: "a1" },
|
||||
{ hash: "AA000002", title: "second phase", name: "p2", description: "second", acceptance: "a2" },
|
||||
];
|
||||
expect(solveIssueModerator(makeCtx(20, [plannerStep(phases)]))).toBe("coder");
|
||||
expect(solveIssueModerator(makeCtx(20, [plannerStep(phases), coderStep("p1")]))).toBe("coder");
|
||||
@@ -191,10 +191,10 @@ describe("solveIssueModerator", () => {
|
||||
|
||||
test("one-shot coder reports only last phase name → reviewer (moderator treats as all phases done)", () => {
|
||||
const phases: PlannerMeta["phases"] = [
|
||||
{ name: "setup-branch", description: "branch", acceptance: "branch exists" },
|
||||
{ name: "write-tests", description: "tests", acceptance: "tests pass" },
|
||||
{ name: "verify", description: "verify", acceptance: "ok" },
|
||||
{ name: "commit-and-pr", description: "pr", acceptance: "pr open" },
|
||||
{ hash: "BB000001", title: "setup branch", name: "setup-branch", description: "branch", acceptance: "branch exists" },
|
||||
{ hash: "BB000002", title: "write tests", name: "write-tests", description: "tests", acceptance: "tests pass" },
|
||||
{ hash: "BB000003", title: "verify", name: "verify", description: "verify", acceptance: "ok" },
|
||||
{ hash: "BB000004", title: "commit and pr", name: "commit-and-pr", description: "pr", acceptance: "pr open" },
|
||||
];
|
||||
expect(
|
||||
solveIssueModerator(makeCtx(20, [plannerStep(phases), coderStep("commit-and-pr")])),
|
||||
@@ -203,8 +203,8 @@ describe("solveIssueModerator", () => {
|
||||
|
||||
test("completedPhase sentinel when not a planned name → reviewer", () => {
|
||||
const phases: PlannerMeta["phases"] = [
|
||||
{ name: "p1", description: "first", acceptance: "a1" },
|
||||
{ name: "p2", description: "second", acceptance: "a2" },
|
||||
{ hash: "CC000001", title: "first phase", name: "p1", description: "first", acceptance: "a1" },
|
||||
{ hash: "CC000002", title: "second phase", name: "p2", description: "second", acceptance: "a2" },
|
||||
];
|
||||
expect(solveIssueModerator(makeCtx(20, [plannerStep(phases), coderStep("all-done")]))).toBe(
|
||||
"reviewer",
|
||||
@@ -213,8 +213,8 @@ describe("solveIssueModerator", () => {
|
||||
|
||||
test("incomplete phases → END when max rounds exhausted", () => {
|
||||
const phases: PlannerMeta["phases"] = [
|
||||
{ name: "p1", description: "first", acceptance: "a1" },
|
||||
{ name: "p2", description: "second", acceptance: "a2" },
|
||||
{ hash: "DD000001", title: "first phase", name: "p1", description: "first", acceptance: "a1" },
|
||||
{ hash: "DD000002", title: "second phase", name: "p2", description: "second", acceptance: "a2" },
|
||||
];
|
||||
const steps: ModeratorContext<SolveIssueMeta>["steps"] = [plannerStep(phases), coderStep("p1")];
|
||||
expect(solveIssueModerator(makeCtx(3, steps))).toBe(END);
|
||||
|
||||
Reference in New Issue
Block a user