From da6bcb10d6fcaf94d86a60bf020aac25968f6325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 11 May 2026 03:34:10 +0000 Subject: [PATCH] feat(workflow): add declarative ModeratorTable type and migrate templates Migrate workflow-template-develop and workflow-template-solve-issue moderators to use the declarative ModeratorTable/tableToModerator pattern. Update workflow-runtime re-exports and workflow-execute engine to use renamed types. Fixes #172 --- .../src/commands/serve/routes-live.ts | 7 +- .../src/commands/serve/routes-thread.ts | 7 +- .../cli-workflow/src/commands/serve/serve.ts | 6 +- packages/workflow-dashboard/src/app.tsx | 11 +- .../src/components/login.tsx | 5 +- .../src/components/markdown.tsx | 29 +- .../src/components/record-card.tsx | 4 +- .../workflow-dashboard/src/use-hash-route.ts | 9 +- .../workflow-execute/src/engine/engine.ts | 6 +- .../workflow-execute/src/engine/supervisor.ts | 6 +- packages/workflow-execute/src/engine/types.ts | 2 +- packages/workflow-gateway/src/index.ts | 12 +- .../__tests__/moderator-table.test.ts | 276 +++++++++--------- .../workflow-protocol/src/moderator-table.ts | 32 +- packages/workflow-protocol/src/types.ts | 14 +- packages/workflow-runtime/src/index.ts | 6 +- .../workflow-template-develop/bundle-entry.ts | 9 +- .../src/moderator.ts | 137 +++++---- .../src/moderator.ts | 30 +- 19 files changed, 330 insertions(+), 278 deletions(-) diff --git a/packages/cli-workflow/src/commands/serve/routes-live.ts b/packages/cli-workflow/src/commands/serve/routes-live.ts index 9cdd271..6321ec0 100644 --- a/packages/cli-workflow/src/commands/serve/routes-live.ts +++ b/packages/cli-workflow/src/commands/serve/routes-live.ts @@ -118,7 +118,12 @@ async function emitRecordsForHead(params: { params.eventId.n++; await params.stream.writeSSE({ event: "record", - data: JSON.stringify({ type: "workflow-result", returnCode: wf.returnCode, content: wf.summary, timestamp: null }), + data: JSON.stringify({ + type: "workflow-result", + returnCode: wf.returnCode, + content: wf.summary, + timestamp: null, + }), id: String(params.eventId.n), }); return true; diff --git a/packages/cli-workflow/src/commands/serve/routes-thread.ts b/packages/cli-workflow/src/commands/serve/routes-thread.ts index 1bb3a51..14294b1 100644 --- a/packages/cli-workflow/src/commands/serve/routes-thread.ts +++ b/packages/cli-workflow/src/commands/serve/routes-thread.ts @@ -137,7 +137,12 @@ export function createThreadRoutes(storageRoot: string): Hono { activityTs: 0, head: resolved.head, }; - const records = await buildThreadDetailRecords(storageRoot, resolved, runningMarkerPresent, statusRow); + const records = await buildThreadDetailRecords( + storageRoot, + resolved, + runningMarkerPresent, + statusRow, + ); return c.json({ threadId, records }); }); diff --git a/packages/cli-workflow/src/commands/serve/serve.ts b/packages/cli-workflow/src/commands/serve/serve.ts index 88df855..684f340 100644 --- a/packages/cli-workflow/src/commands/serve/serve.ts +++ b/packages/cli-workflow/src/commands/serve/serve.ts @@ -16,7 +16,11 @@ import type { ServeOptions } from "./types.js"; const DEFAULT_GATEWAY_URL = "https://workflow-gateway.shazhou.workers.dev"; const HEARTBEAT_INTERVAL_MS = 60_000; -export function startServer(storageRoot: string, options: ServeOptions, agentToken: string | null): void { +export function startServer( + storageRoot: string, + options: ServeOptions, + agentToken: string | null, +): void { const app = createApp(storageRoot, agentToken); const server = serve({ diff --git a/packages/workflow-dashboard/src/app.tsx b/packages/workflow-dashboard/src/app.tsx index 8ee3139..6f58391 100644 --- a/packages/workflow-dashboard/src/app.tsx +++ b/packages/workflow-dashboard/src/app.tsx @@ -20,7 +20,16 @@ export function App() { return (
- { clearApiKey(); setAuthed(false); }} /> + { + clearApiKey(); + setAuthed(false); + }} + />
setShowRun(true)} />
diff --git a/packages/workflow-dashboard/src/components/login.tsx b/packages/workflow-dashboard/src/components/login.tsx index fb86af7..2f88ec3 100644 --- a/packages/workflow-dashboard/src/components/login.tsx +++ b/packages/workflow-dashboard/src/components/login.tsx @@ -44,7 +44,10 @@ export function LoginPage({ onLogin }: Props) { } return ( -
+
> | null = null; -const LANGS: BundledLanguage[] = ["typescript", "javascript", "json", "yaml", "bash", "python", "markdown"]; +const LANGS: BundledLanguage[] = [ + "typescript", + "javascript", + "json", + "yaml", + "bash", + "python", + "markdown", +]; function getHighlighter(): Promise> { if (highlighterPromise === null) { @@ -32,7 +45,9 @@ function CodeBlock({ className, children }: { className?: string; children?: Rea setHtml(null); } }); - return () => { cancelled = true; }; + return () => { + cancelled = true; + }; }, [code, lang]); if (html !== null) { @@ -46,7 +61,10 @@ function CodeBlock({ className, children }: { className?: string; children?: Rea } return ( -
+    
       {code}
     
); @@ -100,7 +118,8 @@ export function Markdown({ content }: { content: string }) { ); }, - }}> + }} + > {content}
diff --git a/packages/workflow-dashboard/src/components/record-card.tsx b/packages/workflow-dashboard/src/components/record-card.tsx index cb966fc..677ddc1 100644 --- a/packages/workflow-dashboard/src/components/record-card.tsx +++ b/packages/workflow-dashboard/src/components/record-card.tsx @@ -93,9 +93,7 @@ function ResultCard({ record }: { record: WorkflowResultRecord }) { >
{success ? "✅" : "❌"} - - {success ? "Completed" : "Failed"} - + {success ? "Completed" : "Failed"} ; @@ -63,7 +63,7 @@ export async function runSupervisor( }; }, systemPromptForStructuredTool: (structuredToolName) => - `You supervise a multi-step workflow. Decide whether the thread should keep running or halt. Reply with "continue" when the thread is making progress toward the task, or "stop" when it is finished, looping, or no longer making progress. Call the ${structuredToolName} tool with JSON arguments matching the schema, or reply with only a JSON object such as {"decision":"stop"}.`, + `You supervise a multi-step workflow. Your job is to detect pathological situations — NOT to decide when the workflow is "done" (that is the moderator's job). Reply with "continue" when the thread is making progress or following its normal role sequence. Reply with "kill" ONLY when the thread is stuck in an infinite loop, producing repetitive/meaningless output, or has clearly gone off the rails. Call the ${structuredToolName} tool with JSON arguments matching the schema, or reply with only a JSON object such as {"decision":"continue"}.`, toolHandler: async (call) => `Unknown tool: ${call.function.name}`, }); diff --git a/packages/workflow-execute/src/engine/types.ts b/packages/workflow-execute/src/engine/types.ts index 0b878ac..1f9e6f6 100644 --- a/packages/workflow-execute/src/engine/types.ts +++ b/packages/workflow-execute/src/engine/types.ts @@ -2,7 +2,7 @@ import type { CasStore } from "@uncaged/workflow-cas"; import type { RoleOutput } from "@uncaged/workflow-runtime"; import type { Result } from "@uncaged/workflow-util"; -export type SupervisorDecision = "continue" | "stop"; +export type SupervisorDecision = "continue" | "kill"; export type ExecuteThreadIo = { threadId: string; diff --git a/packages/workflow-gateway/src/index.ts b/packages/workflow-gateway/src/index.ts index 387e5e8..273f514 100644 --- a/packages/workflow-gateway/src/index.ts +++ b/packages/workflow-gateway/src/index.ts @@ -33,7 +33,10 @@ app.use("/api/*", async (c, next) => { await next(); }); -function checkDashboardAuth(c: { req: { header: (n: string) => string | undefined; query: (n: string) => string | undefined }; env: Env["Bindings"] }): boolean { +function checkDashboardAuth(c: { + req: { header: (n: string) => string | undefined; query: (n: string) => string | undefined }; + env: Env["Bindings"]; +}): boolean { const bearer = c.req.header("Authorization")?.replace("Bearer ", ""); const query = c.req.query("key"); const key = bearer ?? query; @@ -45,7 +48,12 @@ app.get("/healthz", (c) => c.json({ ok: true })); // ── Register / heartbeat ──────────────────────────────────────────── app.post("/register", async (c) => { - const body = await c.req.json<{ name?: string; url?: string; secret?: string; agentToken?: string }>(); + const body = await c.req.json<{ + name?: string; + url?: string; + secret?: string; + agentToken?: string; + }>(); const { name, url, secret, agentToken } = body; if (!name || !url) { diff --git a/packages/workflow-protocol/__tests__/moderator-table.test.ts b/packages/workflow-protocol/__tests__/moderator-table.test.ts index f06a73a..ff534c3 100644 --- a/packages/workflow-protocol/__tests__/moderator-table.test.ts +++ b/packages/workflow-protocol/__tests__/moderator-table.test.ts @@ -1,158 +1,152 @@ import { describe, expect, test } from "bun:test"; + import { tableToModerator } from "../src/moderator-table.js"; +import type { ModeratorContext, ModeratorTable, StartStep } from "../src/types.js"; import { END, START } from "../src/types.js"; -import type { - ModeratorContext, - ModeratorTable, - RoleMeta, - StartStep, -} from "../src/types.js"; type TestMeta = { - planner: { plan: string }; - coder: { code: string }; - reviewer: { approved: boolean }; + planner: { plan: string }; + coder: { code: string }; + reviewer: { approved: boolean }; }; -function makeCtx( - roles: (keyof TestMeta & string)[], -): ModeratorContext { - const steps = roles.map((role, i) => ({ - role, - meta: {} as TestMeta[typeof role], - contentHash: `hash-${i}`, - refs: [], - timestamp: Date.now() + i, - })); - return { - threadId: "test-thread", - depth: 0, - start: { - role: START, - content: "test", - meta: { maxRounds: 10 }, - timestamp: Date.now(), - } as StartStep, - steps, - }; +function makeCtx(roles: (keyof TestMeta & string)[]): ModeratorContext { + const steps = roles.map((role, i) => ({ + role, + meta: {} as TestMeta[typeof role], + contentHash: `hash-${i}`, + refs: [], + timestamp: Date.now() + i, + })); + return { + threadId: "test-thread", + depth: 0, + start: { + role: START, + content: "test", + meta: { maxRounds: 10 }, + timestamp: Date.now(), + } as StartStep, + steps, + }; } describe("tableToModerator", () => { - test("START -> role A (FALLBACK) returns A on first call", () => { - const table: ModeratorTable = { - [START]: [{ condition: "FALLBACK", role: "planner" }], - planner: [], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - expect(mod(makeCtx([]))).toBe("planner"); - }); + test("START -> role A (FALLBACK) returns A on first call", () => { + const table: ModeratorTable = { + [START]: [{ condition: "FALLBACK", role: "planner" }], + planner: [], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + expect(mod(makeCtx([]))).toBe("planner"); + }); - test("condition true wins over FALLBACK", () => { - const table: ModeratorTable = { - [START]: [ - { - condition: { - name: "always", - description: "always true", - check: () => true, - }, - role: "planner", - }, - { condition: "FALLBACK", role: "coder" }, - ], - planner: [], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - expect(mod(makeCtx([]))).toBe("planner"); - }); + test("condition true wins over FALLBACK", () => { + const table: ModeratorTable = { + [START]: [ + { + condition: { + name: "always", + description: "always true", + check: () => true, + }, + role: "planner", + }, + { condition: "FALLBACK", role: "coder" }, + ], + planner: [], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + expect(mod(makeCtx([]))).toBe("planner"); + }); - test("condition false falls through to FALLBACK", () => { - const table: ModeratorTable = { - [START]: [ - { - condition: { - name: "never", - description: "always false", - check: () => false, - }, - role: "planner", - }, - { condition: "FALLBACK", role: "coder" }, - ], - planner: [], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - expect(mod(makeCtx([]))).toBe("coder"); - }); + test("condition false falls through to FALLBACK", () => { + const table: ModeratorTable = { + [START]: [ + { + condition: { + name: "never", + description: "always false", + check: () => false, + }, + role: "planner", + }, + { condition: "FALLBACK", role: "coder" }, + ], + planner: [], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + expect(mod(makeCtx([]))).toBe("coder"); + }); - test("no matching transitions returns END", () => { - const table: ModeratorTable = { - [START]: [ - { - condition: { - name: "never", - description: "always false", - check: () => false, - }, - role: "planner", - }, - ], - planner: [], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - expect(mod(makeCtx([]))).toBe(END); - }); + test("no matching transitions returns END", () => { + const table: ModeratorTable = { + [START]: [ + { + condition: { + name: "never", + description: "always false", + check: () => false, + }, + role: "planner", + }, + ], + planner: [], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + expect(mod(makeCtx([]))).toBe(END); + }); - test("multi-step: A -> FALLBACK END returns END after A", () => { - const table: ModeratorTable = { - [START]: [{ condition: "FALLBACK", role: "planner" }], - planner: [{ condition: "FALLBACK", role: END }], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - expect(mod(makeCtx(["planner"]))).toBe(END); - }); + test("multi-step: A -> FALLBACK END returns END after A", () => { + const table: ModeratorTable = { + [START]: [{ condition: "FALLBACK", role: "planner" }], + planner: [{ condition: "FALLBACK", role: END }], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + expect(mod(makeCtx(["planner"]))).toBe(END); + }); - test("role not in table returns END", () => { - const table: ModeratorTable = { - [START]: [{ condition: "FALLBACK", role: "planner" }], - planner: [{ condition: "FALLBACK", role: "coder" }], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - // coder has empty transitions array - expect(mod(makeCtx(["planner", "coder"]))).toBe(END); - }); + test("role not in table returns END", () => { + const table: ModeratorTable = { + [START]: [{ condition: "FALLBACK", role: "planner" }], + planner: [{ condition: "FALLBACK", role: "coder" }], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + // coder has empty transitions array + expect(mod(makeCtx(["planner", "coder"]))).toBe(END); + }); - test("condition receives ctx", () => { - const table: ModeratorTable = { - [START]: [ - { - condition: { - name: "has-steps", - description: "checks ctx.steps", - check: (ctx) => ctx.steps.length > 0, - }, - role: "coder", - }, - { condition: "FALLBACK", role: "planner" }, - ], - planner: [], - coder: [], - reviewer: [], - }; - const mod = tableToModerator(table); - // No steps -> condition false -> FALLBACK -> planner - expect(mod(makeCtx([]))).toBe("planner"); - }); + test("condition receives ctx", () => { + const table: ModeratorTable = { + [START]: [ + { + condition: { + name: "has-steps", + description: "checks ctx.steps", + check: (ctx) => ctx.steps.length > 0, + }, + role: "coder", + }, + { condition: "FALLBACK", role: "planner" }, + ], + planner: [], + coder: [], + reviewer: [], + }; + const mod = tableToModerator(table); + // No steps -> condition false -> FALLBACK -> planner + expect(mod(makeCtx([]))).toBe("planner"); + }); }); diff --git a/packages/workflow-protocol/src/moderator-table.ts b/packages/workflow-protocol/src/moderator-table.ts index e43b37f..b83bf3f 100644 --- a/packages/workflow-protocol/src/moderator-table.ts +++ b/packages/workflow-protocol/src/moderator-table.ts @@ -1,24 +1,22 @@ import type { Moderator, ModeratorTable, RoleMeta } from "./types.js"; import { END, START } from "./types.js"; -export function tableToModerator( - table: ModeratorTable, -): Moderator { - return (ctx) => { - const lastStep = ctx.steps.length > 0 ? ctx.steps[ctx.steps.length - 1] : null; - const currentRole: string = lastStep ? lastStep.role : START; +export function tableToModerator(table: ModeratorTable): Moderator { + return (ctx) => { + const lastStep = ctx.steps.length > 0 ? ctx.steps[ctx.steps.length - 1] : null; + const currentRole: string = lastStep ? lastStep.role : START; - const transitions = (table as Record)[currentRole]; - if (!transitions) { - return END; - } + const transitions = (table as Record)[currentRole]; + if (!transitions) { + return END; + } - for (const transition of transitions) { - if (transition.condition === "FALLBACK" || transition.condition.check(ctx)) { - return transition.role; - } - } + for (const transition of transitions) { + if (transition.condition === "FALLBACK" || transition.condition.check(ctx)) { + return transition.role; + } + } - return END; - }; + return END; + }; } diff --git a/packages/workflow-protocol/src/types.ts b/packages/workflow-protocol/src/types.ts index 3034344..00f412a 100644 --- a/packages/workflow-protocol/src/types.ts +++ b/packages/workflow-protocol/src/types.ts @@ -172,21 +172,21 @@ export type WorkflowDefinition = { // ── Declarative Moderator Table ──────────────────────────────────── export type ModeratorCondition = { - name: string; - description: string; - check: (ctx: ModeratorContext) => boolean; + name: string; + description: string; + check: (ctx: ModeratorContext) => boolean; }; export type FALLBACK = "FALLBACK"; export type ModeratorTransition = { - condition: ModeratorCondition | FALLBACK; - role: (keyof M & string) | typeof END; + condition: ModeratorCondition | FALLBACK; + role: (keyof M & string) | typeof END; }; export type ModeratorTable = Record< - (keyof M & string) | typeof START, - ModeratorTransition[] + (keyof M & string) | typeof START, + ModeratorTransition[] >; // ── Advance Outcome ──────────────────────────────────────────────── diff --git a/packages/workflow-runtime/src/index.ts b/packages/workflow-runtime/src/index.ts index 268d675..9d41fe8 100644 --- a/packages/workflow-runtime/src/index.ts +++ b/packages/workflow-runtime/src/index.ts @@ -9,9 +9,13 @@ export type { ExtractContext, ExtractFn, ExtractResult, + FALLBACK, LlmProvider, Moderator, + ModeratorCondition, ModeratorContext, + ModeratorTable, + ModeratorTransition, Result, RoleDefinition, RoleMeta, @@ -28,4 +32,4 @@ export type { WorkflowRoleSchema, WorkflowRuntime, } from "./types.js"; -export { END, START } from "./types.js"; +export { END, START, tableToModerator } from "./types.js"; diff --git a/packages/workflow-template-develop/bundle-entry.ts b/packages/workflow-template-develop/bundle-entry.ts index 690a5ed..905c1d7 100644 --- a/packages/workflow-template-develop/bundle-entry.ts +++ b/packages/workflow-template-develop/bundle-entry.ts @@ -1,10 +1,10 @@ /** * develop bundle entry — 小橘 🍊 */ -import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js"; -import { createWorkflow } from "@uncaged/workflow-runtime"; -import { createExtract } from "@uncaged/workflow-execute"; import { createHermesAgent } from "@uncaged/workflow-agent-hermes"; +import { createExtract } from "@uncaged/workflow-execute"; +import { createWorkflow } from "@uncaged/workflow-runtime"; +import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js"; function requireEnv(name: string): string { const value = process.env[name]; @@ -23,7 +23,8 @@ function optionalEnv(name: string): string | null { } const provider = { - baseUrl: optionalEnv("WORKFLOW_LLM_BASE_URL") ?? "https://dashscope.aliyuncs.com/compatible-mode/v1", + baseUrl: + optionalEnv("WORKFLOW_LLM_BASE_URL") ?? "https://dashscope.aliyuncs.com/compatible-mode/v1", apiKey: requireEnv("WORKFLOW_LLM_API_KEY"), model: optionalEnv("WORKFLOW_LLM_MODEL") ?? "qwen-plus", }; diff --git a/packages/workflow-template-develop/src/moderator.ts b/packages/workflow-template-develop/src/moderator.ts index e2431c4..c56eff9 100644 --- a/packages/workflow-template-develop/src/moderator.ts +++ b/packages/workflow-template-develop/src/moderator.ts @@ -1,8 +1,15 @@ -import type { Moderator, ModeratorContext } from "@uncaged/workflow-runtime"; -import { END } from "@uncaged/workflow-runtime"; +import { + END, + type ModeratorCondition, + type ModeratorTable, + START, + tableToModerator, +} from "@uncaged/workflow-runtime"; import type { DevelopMeta } from "./roles.js"; +// ── Helpers ──────────────────────────────────────────────────────── + function coderFinishedAllPlannedPhases( phases: ReadonlyArray<{ hash: string }>, coderCompletedPhases: ReadonlyArray, @@ -22,68 +29,72 @@ function coderFinishedAllPlannedPhases( return false; } -function nextAfterCoder( - ctx: ModeratorContext, - maxRounds: number, -): (keyof DevelopMeta & string) | typeof END { - const plannerStep = ctx.steps.find((s) => s.role === "planner"); - if (plannerStep === undefined) { - return "reviewer"; - } - const phases = plannerStep.meta.phases; - const coderCompletedPhases = ctx.steps - .filter((s) => s.role === "coder") - .map((s) => s.meta.completedPhase); - const allDone = coderFinishedAllPlannedPhases(phases, coderCompletedPhases); - if (allDone) { - return "reviewer"; - } - if (ctx.steps.length < maxRounds - 1) { - return "coder"; - } - return END; -} +// ── Conditions ───────────────────────────────────────────────────── -export const developModerator: Moderator = (ctx) => { - const maxRounds = ctx.start.meta.maxRounds; - - if (ctx.steps.length === 0) { - return "planner"; - } - - const last = ctx.steps[ctx.steps.length - 1]; - - if (last.role === "planner") { - return "coder"; - } - - if (last.role === "coder") { - return nextAfterCoder(ctx, maxRounds); - } - - if (last.role === "reviewer") { - if (last.meta.status === "approved") { - return "tester"; +const allPhasesComplete: ModeratorCondition = { + name: "allPhasesComplete", + description: "All planned phases have been completed by the coder", + check: (ctx) => { + const plannerStep = ctx.steps.find((s) => s.role === "planner"); + if (plannerStep === undefined) { + return true; } - if (ctx.steps.length < maxRounds - 1) { - return "coder"; + const phases = plannerStep.meta.phases; + if (!Array.isArray(phases)) { + return true; } - return END; - } - - if (last.role === "tester") { - if (last.meta.status === "passed") { - return "committer"; - } - if (ctx.steps.length < maxRounds - 1) { - return "coder"; - } - return END; - } - - if (last.role === "committer") { - return END; - } - - return END; + const coderCompletedPhases = ctx.steps + .filter((s) => s.role === "coder") + .map((s) => s.meta.completedPhase); + return coderFinishedAllPlannedPhases(phases, coderCompletedPhases); + }, }; + +const hasRoundsRemaining: ModeratorCondition = { + name: "hasRoundsRemaining", + description: "There are rounds remaining before hitting maxRounds", + check: (ctx) => ctx.steps.length < ctx.start.meta.maxRounds - 1, +}; + +const reviewApproved: ModeratorCondition = { + name: "reviewApproved", + description: "The last reviewer approved the changes", + check: (ctx) => { + const last = ctx.steps[ctx.steps.length - 1]; + return last.role === "reviewer" && last.meta.status === "approved"; + }, +}; + +const testsPassed: ModeratorCondition = { + name: "testsPassed", + description: "The last tester reported tests passed", + check: (ctx) => { + const last = ctx.steps[ctx.steps.length - 1]; + return last.role === "tester" && last.meta.status === "passed"; + }, +}; + +// ── Transition Table ─────────────────────────────────────────────── + +const table: ModeratorTable = { + [START]: [{ condition: "FALLBACK", role: "planner" }], + planner: [{ condition: "FALLBACK", role: "coder" }], + coder: [ + { condition: allPhasesComplete, role: "reviewer" }, + { condition: hasRoundsRemaining, role: "coder" }, + { condition: "FALLBACK", role: END }, + ], + reviewer: [ + { condition: reviewApproved, role: "tester" }, + { condition: hasRoundsRemaining, role: "coder" }, + { condition: "FALLBACK", role: END }, + ], + tester: [ + { condition: testsPassed, role: "committer" }, + { condition: hasRoundsRemaining, role: "coder" }, + { condition: "FALLBACK", role: END }, + ], + committer: [{ condition: "FALLBACK", role: END }], +}; + +export const developModerator = tableToModerator(table); diff --git a/packages/workflow-template-solve-issue/src/moderator.ts b/packages/workflow-template-solve-issue/src/moderator.ts index e846998..458d2f6 100644 --- a/packages/workflow-template-solve-issue/src/moderator.ts +++ b/packages/workflow-template-solve-issue/src/moderator.ts @@ -1,26 +1,12 @@ -import type { Moderator } from "@uncaged/workflow-runtime"; -import { END } from "@uncaged/workflow-runtime"; +import { END, type ModeratorTable, START, tableToModerator } from "@uncaged/workflow-runtime"; import type { SolveIssueMeta } from "./roles.js"; -export const solveIssueModerator: Moderator = (ctx) => { - if (ctx.steps.length === 0) { - return "preparer"; - } - - const last = ctx.steps[ctx.steps.length - 1]; - - if (last.role === "preparer") { - return "developer"; - } - - if (last.role === "developer") { - return "submitter"; - } - - if (last.role === "submitter") { - return END; - } - - return END; +const table: ModeratorTable = { + [START]: [{ condition: "FALLBACK", role: "preparer" }], + preparer: [{ condition: "FALLBACK", role: "developer" }], + developer: [{ condition: "FALLBACK", role: "submitter" }], + submitter: [{ condition: "FALLBACK", role: END }], }; + +export const solveIssueModerator = tableToModerator(table);