From 172e9b34cc189bab74eecb7b5f8874d23a5a0e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Thu, 7 May 2026 04:18:42 +0000 Subject: [PATCH] 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 --- packages/workflow-role-coder/src/coder.ts | 2 +- packages/workflow-role-planner/src/planner.ts | 6 +++-- .../__tests__/solve-issue-template.test.ts | 24 +++++++++---------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/workflow-role-coder/src/coder.ts b/packages/workflow-role-coder/src/coder.ts index fe3dc1a..1a88a6c 100644 --- a/packages/workflow-role-coder/src/coder.ts +++ b/packages/workflow-role-coder/src/coder.ts @@ -10,7 +10,7 @@ export const coderMetaSchema = z.object({ export type CoderMeta = z.infer; 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 = { description: diff --git a/packages/workflow-role-planner/src/planner.ts b/packages/workflow-role-planner/src/planner.ts index 6ace1bf..adb12c5 100644 --- a/packages/workflow-role-planner/src/planner.ts +++ b/packages/workflow-role-planner/src/planner.ts @@ -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; 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 = { 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, }; diff --git a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts index 6fbdd47..e1cd604 100644 --- a/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts +++ b/packages/workflow-template-solve-issue/__tests__/solve-issue-template.test.ts @@ -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["steps"] = [plannerStep(phases), coderStep("p1")]; expect(solveIssueModerator(makeCtx(3, steps))).toBe(END);