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:
planner:
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:
type: object
properties:
@@ -16,7 +19,10 @@ roles:
required: [plan, steps]
developer:
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:
type: object
properties:
@@ -29,7 +35,10 @@ roles:
required: [filesChanged, summary]
reviewer:
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:
type: object
properties:
@@ -211,7 +211,10 @@ describe("cmdThreadRead ### Content section", () => {
roles: {
writer: {
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,
},
},
+1 -1
View File
@@ -500,7 +500,7 @@ function formatThreadReadMarkdown(options: {
];
const roleDef = workflow.roles[item.payload.role];
if (roleDef) {
const prompt = roleDef.identity ?? roleDef.systemPrompt ?? "";
const prompt = roleDef.identity;
stepLines.push("", "### Prompt", "", prompt);
}
if (item.payload.detail) {
+5 -13
View File
@@ -67,22 +67,14 @@ async function materializeWorkflowPayload(
`${raw.name}.${roleName}`,
role.outputSchema,
);
const roleDef: RoleDefinition = {
roles[roleName] = {
description: role.description,
systemPrompt: role.systemPrompt ?? null,
identity: role.identity ?? null,
prepare: role.prepare ?? null,
execute: role.execute ?? null,
report: role.report ?? null,
identity: role.identity,
prepare: role.prepare,
execute: role.execute,
report: role.report,
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 {
name: raw.name,
+8 -11
View File
@@ -16,17 +16,14 @@ function isRoleDefinition(value: unknown): boolean {
}
const outputSchema = value.outputSchema;
const schemaOk = isRecord(outputSchema) && typeof outputSchema.type === "string";
const hasSystemPrompt =
value.systemPrompt === undefined || value.systemPrompt === null || typeof value.systemPrompt === "string";
const hasIdentity =
value.identity === undefined || value.identity === null || typeof value.identity === "string";
const hasPrepare =
value.prepare === undefined || value.prepare === null || typeof value.prepare === "string";
const hasExecute =
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;
return (
typeof value.description === "string" &&
typeof value.identity === "string" &&
typeof value.prepare === "string" &&
typeof value.execute === "string" &&
typeof value.report === "string" &&
schemaOk
);
}
function isConditionDefinition(value: unknown): boolean {
@@ -3,10 +3,9 @@ import type { RoleDefinition } from "@uncaged/workflow-protocol";
import { buildRolePrompt } from "../src/build-role-prompt.js";
describe("buildRolePrompt", () => {
test("four-phase: all fields present", () => {
test("all fields present", () => {
const role: RoleDefinition = {
description: "A coder",
systemPrompt: "legacy prompt",
identity: "You are a senior developer.",
prepare: "Load cursor-agent skill.",
execute: "Implement the feature.",
@@ -22,40 +21,15 @@ describe("buildRolePrompt", () => {
expect(result).toContain("Implement the feature.");
expect(result).toContain("## Report");
expect(result).toContain("Summarize changes.");
expect(result).not.toContain("legacy prompt");
});
test("legacy: only systemPrompt", () => {
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", () => {
test("empty fields are omitted", () => {
const role: RoleDefinition = {
description: "A reviewer",
systemPrompt: "legacy",
identity: "You are a code reviewer.",
prepare: null,
prepare: "",
execute: "Review the PR diff carefully.",
report: null,
report: "",
outputSchema: "placeholder00000" as string,
};
const result = buildRolePrompt(role);
@@ -63,6 +37,18 @@ describe("buildRolePrompt", () => {
expect(result).toContain("## Execute");
expect(result).not.toContain("## Prepare");
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.
*
* Four-phase mode (identity/prepare/execute/report present):
* Assembles structured sections for each phase.
*
* Legacy mode (only systemPrompt):
* Returns systemPrompt as-is.
*
* When both are present, four-phase fields take priority.
* Assembles structured sections: Identity, Prepare, Execute, Report.
* Empty strings are omitted from the output.
*/
export function buildRolePrompt(role: RoleDefinition): string {
const hasFourPhase =
role.identity !== null &&
role.identity !== undefined &&
role.identity !== "";
if (!hasFourPhase) {
return role.systemPrompt ?? "";
}
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}`);
}
if (role.execute !== null && role.execute !== undefined && role.execute !== "") {
if (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}`);
}
+1 -1
View File
@@ -7,7 +7,7 @@ export type AgentContext = ModeratorContext & {
store: Store;
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
* role's output schema. Populated by `createAgent` at run time.
*/
@@ -9,17 +9,26 @@ const solveIssueWorkflow: WorkflowPayload = {
roles: {
planner: {
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",
},
developer: {
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",
},
reviewer: {
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",
},
},
+1 -2
View File
@@ -2,10 +2,9 @@ import type { JSONSchema } from "@uncaged/json-cas";
const ROLE_DEFINITION: JSONSchema = {
type: "object",
required: ["description", "outputSchema"],
required: ["description", "identity", "prepare", "execute", "report", "outputSchema"],
properties: {
description: { type: "string" },
systemPrompt: { type: "string" },
identity: { type: "string" },
prepare: { type: "string" },
execute: { type: "string" },
+4 -5
View File
@@ -18,11 +18,10 @@ export type StepRecord = {
export type RoleDefinition = {
description: string;
systemPrompt: string | null;
identity: string | null;
prepare: string | null;
execute: string | null;
report: string | null;
identity: string;
prepare: string;
execute: string;
report: string;
outputSchema: CasRef;
};