refactor: remove systemPrompt, make four-phase fields required

Breaking change per review:
- Remove systemPrompt from RoleDefinition entirely
- identity/prepare/execute/report are now required (string, not nullable)
- Remove all legacy fallback logic in buildRolePrompt
- Simplify validate.ts, workflow.ts materialize
- Migrate all test fixtures and example workflows

Refs #359
This commit is contained in:
2026-05-21 03:07:56 +00:00
parent c0c8d6499e
commit 192ad656a4
11 changed files with 73 additions and 91 deletions
+12 -3
View File
@@ -3,7 +3,10 @@ description: "End-to-end issue resolution"
roles: roles:
planner: planner:
description: "Creates implementation plan" description: "Creates implementation plan"
systemPrompt: "You are a planning agent. Analyze the issue and create a step-by-step plan." identity: "You are a planning agent. You analyze issues and create step-by-step plans."
prepare: "Read the issue description and any linked context carefully."
execute: "Analyze the issue and create a detailed, actionable implementation plan."
report: "Output the plan summary and list of concrete steps."
outputSchema: outputSchema:
type: object type: object
properties: properties:
@@ -16,7 +19,10 @@ roles:
required: [plan, steps] required: [plan, steps]
developer: developer:
description: "Implements code changes" description: "Implements code changes"
systemPrompt: "You are a developer agent. Implement the plan." identity: "You are a developer agent. You implement code changes according to plans."
prepare: "Load coding tools and review the project structure and conventions."
execute: "Implement the plan. Write code, tests, and ensure existing tests pass."
report: "List all files changed and provide a summary of the implementation."
outputSchema: outputSchema:
type: object type: object
properties: properties:
@@ -29,7 +35,10 @@ roles:
required: [filesChanged, summary] required: [filesChanged, summary]
reviewer: reviewer:
description: "Reviews code changes" description: "Reviews code changes"
systemPrompt: "You are a code reviewer. Review the implementation." identity: "You are a code reviewer. You review implementations for correctness and quality."
prepare: "Review the project's coding standards and conventions."
execute: "Review the implementation against the plan. Check for bugs, edge cases, and style."
report: "Approve or reject with detailed comments explaining your decision."
outputSchema: outputSchema:
type: object type: object
properties: properties:
@@ -211,7 +211,10 @@ describe("cmdThreadRead ### Content section", () => {
roles: { roles: {
writer: { writer: {
description: "Write", description: "Write",
systemPrompt: "You are a writer.", identity: "You are a writer.",
prepare: "",
execute: "Write content as requested.",
report: "Summarize what was written.",
outputSchema: "placeholder00" as CasRef, outputSchema: "placeholder00" as CasRef,
}, },
}, },
+1 -1
View File
@@ -500,7 +500,7 @@ function formatThreadReadMarkdown(options: {
]; ];
const roleDef = workflow.roles[item.payload.role]; const roleDef = workflow.roles[item.payload.role];
if (roleDef) { if (roleDef) {
const prompt = roleDef.identity ?? roleDef.systemPrompt ?? ""; const prompt = roleDef.identity;
stepLines.push("", "### Prompt", "", prompt); stepLines.push("", "### Prompt", "", prompt);
} }
if (item.payload.detail) { if (item.payload.detail) {
+5 -13
View File
@@ -67,22 +67,14 @@ async function materializeWorkflowPayload(
`${raw.name}.${roleName}`, `${raw.name}.${roleName}`,
role.outputSchema, role.outputSchema,
); );
const roleDef: RoleDefinition = { roles[roleName] = {
description: role.description, description: role.description,
systemPrompt: role.systemPrompt ?? null, identity: role.identity,
identity: role.identity ?? null, prepare: role.prepare,
prepare: role.prepare ?? null, execute: role.execute,
execute: role.execute ?? null, report: role.report,
report: role.report ?? null,
outputSchema, outputSchema,
}; };
// Strip null fields so CAS payload stays lean and schema-valid
for (const key of ["systemPrompt", "identity", "prepare", "execute", "report"] as const) {
if (roleDef[key] === null) {
delete (roleDef as Record<string, unknown>)[key];
}
}
roles[roleName] = roleDef;
} }
return { return {
name: raw.name, name: raw.name,
+8 -11
View File
@@ -16,17 +16,14 @@ function isRoleDefinition(value: unknown): boolean {
} }
const outputSchema = value.outputSchema; const outputSchema = value.outputSchema;
const schemaOk = isRecord(outputSchema) && typeof outputSchema.type === "string"; const schemaOk = isRecord(outputSchema) && typeof outputSchema.type === "string";
const hasSystemPrompt = return (
value.systemPrompt === undefined || value.systemPrompt === null || typeof value.systemPrompt === "string"; typeof value.description === "string" &&
const hasIdentity = typeof value.identity === "string" &&
value.identity === undefined || value.identity === null || typeof value.identity === "string"; typeof value.prepare === "string" &&
const hasPrepare = typeof value.execute === "string" &&
value.prepare === undefined || value.prepare === null || typeof value.prepare === "string"; typeof value.report === "string" &&
const hasExecute = schemaOk
value.execute === undefined || value.execute === null || typeof value.execute === "string"; );
const hasReport =
value.report === undefined || value.report === null || typeof value.report === "string";
return typeof value.description === "string" && hasSystemPrompt && hasIdentity && hasPrepare && hasExecute && hasReport && schemaOk;
} }
function isConditionDefinition(value: unknown): boolean { function isConditionDefinition(value: unknown): boolean {
@@ -3,10 +3,9 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol";
import { buildRolePrompt } from "../src/build-role-prompt.js"; import { buildRolePrompt } from "../src/build-role-prompt.js";
describe("buildRolePrompt", () => { describe("buildRolePrompt", () => {
test("four-phase: all fields present", () => { test("all fields present", () => {
const role: RoleDefinition = { const role: RoleDefinition = {
description: "A coder", description: "A coder",
systemPrompt: "legacy prompt",
identity: "You are a senior developer.", identity: "You are a senior developer.",
prepare: "Load cursor-agent skill.", prepare: "Load cursor-agent skill.",
execute: "Implement the feature.", execute: "Implement the feature.",
@@ -22,40 +21,15 @@ describe("buildRolePrompt", () => {
expect(result).toContain("Implement the feature."); expect(result).toContain("Implement the feature.");
expect(result).toContain("## Report"); expect(result).toContain("## Report");
expect(result).toContain("Summarize changes."); expect(result).toContain("Summarize changes.");
expect(result).not.toContain("legacy prompt");
}); });
test("legacy: only systemPrompt", () => { test("empty fields are omitted", () => {
const role: RoleDefinition = {
description: "A planner",
systemPrompt: "You are a planning agent.",
identity: null,
prepare: null,
execute: null,
report: null,
outputSchema: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toBe("You are a planning agent.");
});
test("legacy: no fields at all", () => {
const role = {
description: "Minimal",
outputSchema: "placeholder00000",
} as unknown as RoleDefinition;
const result = buildRolePrompt(role);
expect(result).toBe("");
});
test("mixed: identity present, partial other fields", () => {
const role: RoleDefinition = { const role: RoleDefinition = {
description: "A reviewer", description: "A reviewer",
systemPrompt: "legacy",
identity: "You are a code reviewer.", identity: "You are a code reviewer.",
prepare: null, prepare: "",
execute: "Review the PR diff carefully.", execute: "Review the PR diff carefully.",
report: null, report: "",
outputSchema: "placeholder00000" as string, outputSchema: "placeholder00000" as string,
}; };
const result = buildRolePrompt(role); const result = buildRolePrompt(role);
@@ -63,6 +37,18 @@ describe("buildRolePrompt", () => {
expect(result).toContain("## Execute"); expect(result).toContain("## Execute");
expect(result).not.toContain("## Prepare"); expect(result).not.toContain("## Prepare");
expect(result).not.toContain("## Report"); expect(result).not.toContain("## Report");
expect(result).not.toContain("legacy"); });
test("all empty returns empty string", () => {
const role: RoleDefinition = {
description: "Minimal",
identity: "",
prepare: "",
execute: "",
report: "",
outputSchema: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
expect(result).toBe("");
}); });
}); });
@@ -3,37 +3,25 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol";
/** /**
* Build the role prompt from a RoleDefinition. * Build the role prompt from a RoleDefinition.
* *
* Four-phase mode (identity/prepare/execute/report present): * Assembles structured sections: Identity, Prepare, Execute, Report.
* Assembles structured sections for each phase. * Empty strings are omitted from the output.
*
* Legacy mode (only systemPrompt):
* Returns systemPrompt as-is.
*
* When both are present, four-phase fields take priority.
*/ */
export function buildRolePrompt(role: RoleDefinition): string { export function buildRolePrompt(role: RoleDefinition): string {
const hasFourPhase =
role.identity !== null &&
role.identity !== undefined &&
role.identity !== "";
if (!hasFourPhase) {
return role.systemPrompt ?? "";
}
const sections: string[] = []; const sections: string[] = [];
sections.push(`## Identity\n\n${role.identity}`); if (role.identity !== "") {
sections.push(`## Identity\n\n${role.identity}`);
}
if (role.prepare !== null && role.prepare !== undefined && role.prepare !== "") { if (role.prepare !== "") {
sections.push(`## Prepare\n\n${role.prepare}`); sections.push(`## Prepare\n\n${role.prepare}`);
} }
if (role.execute !== null && role.execute !== undefined && role.execute !== "") { if (role.execute !== "") {
sections.push(`## Execute\n\n${role.execute}`); sections.push(`## Execute\n\n${role.execute}`);
} }
if (role.report !== null && role.report !== undefined && role.report !== "") { if (role.report !== "") {
sections.push(`## Report\n\n${role.report}`); sections.push(`## Report\n\n${role.report}`);
} }
+1 -1
View File
@@ -7,7 +7,7 @@ export type AgentContext = ModeratorContext & {
store: Store; store: Store;
workflow: WorkflowPayload; workflow: WorkflowPayload;
/** /**
* Prepend to the role's systemPrompt when building the agent prompt. * Prepend to the role's prompt when building the agent prompt.
* Contains the frontmatter deliverable format instruction derived from the * Contains the frontmatter deliverable format instruction derived from the
* role's output schema. Populated by `createAgent` at run time. * role's output schema. Populated by `createAgent` at run time.
*/ */
@@ -9,17 +9,26 @@ const solveIssueWorkflow: WorkflowPayload = {
roles: { roles: {
planner: { planner: {
description: "Creates implementation plan", description: "Creates implementation plan",
systemPrompt: "You are a planning agent...", identity: "You are a planning agent.",
prepare: "Review the issue context.",
execute: "Create a step-by-step plan.",
report: "Output the plan and steps.",
outputSchema: "5GWKR8TN1V3JA", outputSchema: "5GWKR8TN1V3JA",
}, },
developer: { developer: {
description: "Implements code changes", description: "Implements code changes",
systemPrompt: "You are a developer agent...", identity: "You are a developer agent.",
prepare: "Load coding tools.",
execute: "Implement the plan.",
report: "List files changed and summary.",
outputSchema: "8CNWT4KR6D1HV", outputSchema: "8CNWT4KR6D1HV",
}, },
reviewer: { reviewer: {
description: "Reviews code changes", description: "Reviews code changes",
systemPrompt: "You are a code reviewer...", identity: "You are a code reviewer.",
prepare: "Review project conventions.",
execute: "Review the implementation.",
report: "Approve or reject with comments.",
outputSchema: "1VPBG9SM5E7WK", outputSchema: "1VPBG9SM5E7WK",
}, },
}, },
+1 -2
View File
@@ -2,10 +2,9 @@ import type { JSONSchema } from "@uncaged/json-cas";
const ROLE_DEFINITION: JSONSchema = { const ROLE_DEFINITION: JSONSchema = {
type: "object", type: "object",
required: ["description", "outputSchema"], required: ["description", "identity", "prepare", "execute", "report", "outputSchema"],
properties: { properties: {
description: { type: "string" }, description: { type: "string" },
systemPrompt: { type: "string" },
identity: { type: "string" }, identity: { type: "string" },
prepare: { type: "string" }, prepare: { type: "string" },
execute: { type: "string" }, execute: { type: "string" },
+4 -5
View File
@@ -18,11 +18,10 @@ export type StepRecord = {
export type RoleDefinition = { export type RoleDefinition = {
description: string; description: string;
systemPrompt: string | null; identity: string;
identity: string | null; prepare: string;
prepare: string | null; execute: string;
execute: string | null; report: string;
report: string | null;
outputSchema: CasRef; outputSchema: CasRef;
}; };