refactor: replace maxRounds with supervisor check interval

Removes maxRounds as a hard stop limit from the entire stack. The supervisor
(already configured via workflow.yaml supervisorInterval) is now the sole
termination authority.

Changes across 27 files in 11 packages:
- workflow-protocol: StartStep.meta is now empty, StartNodePayload drops maxRounds
- workflow-cas: isStartPayload no longer checks maxRounds
- workflow-execute: engine, worker, fork-thread all stripped of maxRounds plumbing
- cli-workflow: --max-rounds flag removed from CLI and HTTP API
- workflow-runtime: build-context and create-workflow no longer reference maxRounds
- workflow-dashboard: UI no longer sends maxRounds
- workflow-template-develop/solve-issue: moderator no longer checks rounds remaining
- All tests updated

Fixes #185
This commit is contained in:
2026-05-11 08:51:35 +00:00
parent 0021596ff0
commit 2b587612d5
32 changed files with 84 additions and 204 deletions
@@ -13,23 +13,20 @@ const DEFAULT_PHASES: PlannerMeta["phases"] = [
},
];
function makeStart(maxRounds: number): ModeratorContext<DevelopMeta>["start"] {
function makeStart(): ModeratorContext<DevelopMeta>["start"] {
return {
role: START,
content: "Implement the feature",
meta: { maxRounds },
meta: {},
timestamp: 0,
};
}
function makeCtx(
maxRounds: number,
steps: ModeratorContext<DevelopMeta>["steps"],
): ModeratorContext<DevelopMeta> {
function makeCtx(steps: ModeratorContext<DevelopMeta>["steps"]): ModeratorContext<DevelopMeta> {
return {
threadId: "01TEST000000000000000000TR",
depth: 0,
start: makeStart(maxRounds),
start: makeStart(),
steps,
};
}
@@ -90,20 +87,18 @@ function committerStep(meta: CommitterMeta): RoleStep<DevelopMeta> {
describe("developModerator", () => {
test("routes initial → planner → coder → reviewer → tester → committer → END", () => {
expect(developModerator(makeCtx(20, []))).toBe("planner");
expect(developModerator(makeCtx(20, [plannerStep()]))).toBe("coder");
expect(developModerator(makeCtx(20, [plannerStep(), coderStep()]))).toBe("reviewer");
expect(developModerator(makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
expect(developModerator(makeCtx([]))).toBe("planner");
expect(developModerator(makeCtx([plannerStep()]))).toBe("coder");
expect(developModerator(makeCtx([plannerStep(), coderStep()]))).toBe("reviewer");
expect(developModerator(makeCtx([plannerStep(), coderStep(), reviewerStep(true)]))).toBe(
"tester",
);
expect(
developModerator(
makeCtx(20, [plannerStep(), coderStep(), reviewerStep(true), testerStep(true)]),
),
developModerator(makeCtx([plannerStep(), coderStep(), reviewerStep(true), testerStep(true)])),
).toBe("committer");
expect(
developModerator(
makeCtx(20, [
makeCtx([
plannerStep(),
coderStep(),
reviewerStep(true),
@@ -120,7 +115,7 @@ describe("developModerator", () => {
coderStep(),
reviewerStep(false),
];
expect(developModerator(makeCtx(20, steps))).toBe("coder");
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("reviewer rejects → END when max rounds exhausted", () => {
@@ -129,7 +124,7 @@ describe("developModerator", () => {
coderStep(),
reviewerStep(false),
];
expect(developModerator(makeCtx(4, steps))).toBe(END);
expect(developModerator(makeCtx(steps))).toBe(END);
});
test("tester failed → coder retry when budget allows", () => {
@@ -139,7 +134,7 @@ describe("developModerator", () => {
reviewerStep(true),
testerStep(false),
];
expect(developModerator(makeCtx(20, steps))).toBe("coder");
expect(developModerator(makeCtx(steps))).toBe("coder");
});
test("tester failed → END when max rounds exhausted", () => {
@@ -149,7 +144,7 @@ describe("developModerator", () => {
reviewerStep(true),
testerStep(false),
];
expect(developModerator(makeCtx(5, steps))).toBe(END);
expect(developModerator(makeCtx(steps))).toBe(END);
});
test("multiple planner phases → coder until all complete, then reviewer", () => {
@@ -157,13 +152,11 @@ describe("developModerator", () => {
{ hash: "AA000001", title: "first phase" },
{ hash: "AA000002", title: "second phase" },
];
expect(developModerator(makeCtx(20, [plannerStep(phases)]))).toBe("coder");
expect(developModerator(makeCtx(20, [plannerStep(phases), coderStep("AA000001")]))).toBe(
"coder",
);
expect(developModerator(makeCtx([plannerStep(phases)]))).toBe("coder");
expect(developModerator(makeCtx([plannerStep(phases), coderStep("AA000001")]))).toBe("coder");
expect(
developModerator(
makeCtx(20, [plannerStep(phases), coderStep("AA000001"), coderStep("AA000002")]),
makeCtx([plannerStep(phases), coderStep("AA000001"), coderStep("AA000002")]),
),
).toBe("reviewer");
});
@@ -175,7 +168,7 @@ describe("developModerator", () => {
{ hash: "BB000003", title: "verify" },
{ hash: "BB000004", title: "polish" },
];
expect(developModerator(makeCtx(20, [plannerStep(phases), coderStep("BB000004")]))).toBe(
expect(developModerator(makeCtx([plannerStep(phases), coderStep("BB000004")]))).toBe(
"reviewer",
);
});
@@ -185,9 +178,7 @@ describe("developModerator", () => {
{ hash: "CC000001", title: "first phase" },
{ hash: "CC000002", title: "second phase" },
];
expect(developModerator(makeCtx(20, [plannerStep(phases), coderStep("all-done")]))).toBe(
"coder",
);
expect(developModerator(makeCtx([plannerStep(phases), coderStep("all-done")]))).toBe("coder");
});
test("incomplete phases → END when max rounds exhausted", () => {
@@ -199,7 +190,7 @@ describe("developModerator", () => {
plannerStep(phases),
coderStep("DD000001"),
];
expect(developModerator(makeCtx(3, steps))).toBe(END);
expect(developModerator(makeCtx(steps))).toBe(END);
});
test("committer → END for any committer meta status", () => {
@@ -220,9 +211,9 @@ describe("developModerator", () => {
reviewerStep(true),
testerStep(true),
];
expect(developModerator(makeCtx(20, [...base, committed]))).toBe(END);
expect(developModerator(makeCtx(20, [...base, recoverable]))).toBe(END);
expect(developModerator(makeCtx(20, [...base, unrecoverable]))).toBe(END);
expect(developModerator(makeCtx([...base, committed]))).toBe(END);
expect(developModerator(makeCtx([...base, recoverable]))).toBe(END);
expect(developModerator(makeCtx([...base, unrecoverable]))).toBe(END);
});
});
@@ -52,8 +52,8 @@ const allPhasesComplete: ModeratorCondition<DevelopMeta> = {
const hasRoundsRemaining: ModeratorCondition<DevelopMeta> = {
name: "hasRoundsRemaining",
description: "There are rounds remaining before hitting maxRounds",
check: (ctx) => ctx.steps.length < ctx.start.meta.maxRounds - 1,
description: "Always true — supervisor controls termination",
check: () => true,
};
const reviewApproved: ModeratorCondition<DevelopMeta> = {
@@ -2,7 +2,11 @@ import type { RoleDefinition } from "@uncaged/workflow-runtime";
import * as z from "zod/v4";
export const coderMetaSchema = z.object({
completedPhase: z.string().describe("The planner phase hash finished this round. If multiple phases were completed, use the last finished phase hash."),
completedPhase: z
.string()
.describe(
"The planner phase hash finished this round. If multiple phases were completed, use the last finished phase hash.",
),
filesChanged: z.array(z.string()),
summary: z.string(),
});